To M73: MSE: Reduce amount of frame flushing for common ByPts scenario

Previously, when buffering by PTS, almost all audio frames emitted by
FrameProcessor would be emitted singly, incurring significant overhead
of frequent SourceBufferStream::Append() calls of single frames versus
collections of frames, even when these audio frames really are
continuous.

This change allows the duration of the current frame to be used as a
proxy for (roughly) half of the SourceBufferStream's buffering fudge
room, with the other half already built into the FrameProcessor's
MseTrackBuffer highest_presentation_timestamp(). Because
SourceBufferStream updates its (non-decreasing) fudge room at the start
of Append() servicing, this relaxation in FrameProcessor should not
introduce failures in adjacency checks during SBS::Append() (and
underlying SourceBufferRangeByPts) processing, while allowing simple
continuous-in-DTS (within MSE coded frame processing spec'ed adjacency
logic) and continuous-in-PTS (within our SourceBufferStream's fudge
room) keyframes to not be unduly Appended one-at-at-time to
SourceBufferStream. Note, this change is not specific to audio frames;
video keyframes also satisfying such continuity are also queued for bulk
flushing later in FrameProcessor.

Relevant unit tests now verify the specifics of group start signalling
and the sequence of append calls by FrameProcessor.

BUG=879970,912695
TEST=Multiple updated and new FrameProcessorTests

Change-Id: Ifcfe9fc2062888b1c835380871be355a45b0babe
Reviewed-on: https://chromium-review.googlesource.com/c/1441492
Reviewed-by: Dan Sanders <sandersd@chromium.org>
Commit-Queue: Matthew Wolenetz <wolenetz@chromium.org>
Cr-Commit-Position: refs/heads/master@{#627070}(cherry picked from commit d2ad5ce3165b485156ffff771a3e860b1ab78ad4)

TBR=sandersd@chromium.org

Change-Id: I37f2fa8abe0bc54bca5eb0c248a4d2bed7115769
Reviewed-on: https://chromium-review.googlesource.com/c/1450439
Reviewed-by: Matthew Wolenetz <wolenetz@chromium.org>
Cr-Commit-Position: refs/branch-heads/3683@{#130}
Cr-Branched-From: e51029943e0a38dd794b73caaf6373d5496ae783-refs/heads/master@{#625896}
diff --git a/media/filters/chunk_demuxer.cc b/media/filters/chunk_demuxer.cc
index c29a7a9..39cc4a12 100644
--- a/media/filters/chunk_demuxer.cc
+++ b/media/filters/chunk_demuxer.cc
@@ -154,6 +154,9 @@
 }
 
 bool ChunkDemuxerStream::Append(const StreamParser::BufferQueue& buffers) {
+  if (append_observer_cb_)
+    append_observer_cb_.Run(&buffers);
+
   if (buffers.empty())
     return false;
 
@@ -258,6 +261,10 @@
   DVLOG(2) << "ChunkDemuxerStream::OnStartOfCodedFrameGroup(dts "
            << start_dts.InSecondsF() << ", pts " << start_pts.InSecondsF()
            << ")";
+
+  if (group_start_observer_cb_)
+    group_start_observer_cb_.Run(start_dts, start_pts);
+
   base::AutoLock auto_lock(lock_);
   SBSTREAM_OP(OnStartOfCodedFrameGroup(start_dts, start_pts));
 }
diff --git a/media/filters/chunk_demuxer.h b/media/filters/chunk_demuxer.h
index 5d085b1..5cd48c0 100644
--- a/media/filters/chunk_demuxer.h
+++ b/media/filters/chunk_demuxer.h
@@ -153,6 +153,20 @@
 
   MediaTrack::Id media_track_id() const { return media_track_id_; }
 
+  // Allows tests to verify invocations of Append().
+  using AppendObserverCB = base::RepeatingCallback<void(const BufferQueue*)>;
+  void set_append_observer_for_testing(AppendObserverCB append_observer_cb) {
+    append_observer_cb_ = std::move(append_observer_cb);
+  }
+
+  // Allows tests to verify invocations of OnStartOfCodedFrameGroup().
+  using GroupStartObserverCB =
+      base::RepeatingCallback<void(DecodeTimestamp, base::TimeDelta)>;
+  void set_group_start_observer_for_testing(
+      GroupStartObserverCB group_start_observer_cb) {
+    group_start_observer_cb_ = std::move(group_start_observer_cb);
+  }
+
  private:
   enum State {
     UNINITIALIZED,
@@ -181,6 +195,9 @@
 
   const MediaTrack::Id media_track_id_;
 
+  AppendObserverCB append_observer_cb_;
+  GroupStartObserverCB group_start_observer_cb_;
+
   mutable base::Lock lock_;
   State state_ GUARDED_BY(lock_);
   ReadCB read_cb_ GUARDED_BY(lock_);
diff --git a/media/filters/frame_processor.cc b/media/filters/frame_processor.cc
index cbbe3ff..6c48a49 100644
--- a/media/filters/frame_processor.cc
+++ b/media/filters/frame_processor.cc
@@ -956,13 +956,14 @@
 
       // When buffering by PTS intervals and an otherwise continuous coded frame
       // group (by DTS, and with non-decreasing keyframe PTS) contains a
-      // keyframe with PTS in the future, signal a new coded frame group with
+      // keyframe with PTS in the future significantly far enough that it may be
+      // outside of buffering fudge room, signal a new coded frame group with
       // start time set to the previous highest frame end time in the coded
       // frame group for this track. This lets the stream coalesce a potential
       // gap, and also pass internal buffer adjacency checks.
       signal_new_cfg |=
           track_buffer->highest_presentation_timestamp() != kNoTimestamp &&
-          track_buffer->highest_presentation_timestamp() <
+          track_buffer->highest_presentation_timestamp() + frame->duration() <
               presentation_timestamp;
     }
 
diff --git a/media/filters/frame_processor_unittest.cc b/media/filters/frame_processor_unittest.cc
index 222a3fd..97fe91d 100644
--- a/media/filters/frame_processor_unittest.cc
+++ b/media/filters/frame_processor_unittest.cc
@@ -28,6 +28,7 @@
 #include "media/filters/frame_processor.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
+using ::testing::_;
 using ::testing::InSequence;
 using ::testing::StrictMock;
 using ::testing::Values;
@@ -79,6 +80,14 @@
     ASSERT_NE(kInfiniteDuration, new_duration);
   }
 
+  MOCK_METHOD2(OnAppend,
+               void(const DemuxerStream::Type type,
+                    const BufferQueue* buffers));
+  MOCK_METHOD3(OnGroupStart,
+               void(const DemuxerStream::Type type,
+                    DecodeTimestamp start_dts,
+                    base::TimeDelta start_pts));
+
  private:
   DISALLOW_COPY_AND_ASSIGN(FrameProcessorTestCallbackHelper);
 };
@@ -107,7 +116,8 @@
 
   enum StreamFlags {
     HAS_AUDIO = 1 << 0,
-    HAS_VIDEO = 1 << 1
+    HAS_VIDEO = 1 << 1,
+    OBSERVE_APPENDS_AND_GROUP_STARTS = 1 << 2
   };
 
   void AddTestTracks(int stream_flags) {
@@ -115,14 +125,17 @@
     const bool has_video = (stream_flags & HAS_VIDEO) != 0;
     ASSERT_TRUE(has_audio || has_video);
 
+    const bool setup_observers =
+        (stream_flags & OBSERVE_APPENDS_AND_GROUP_STARTS) != 0;
+
     if (has_audio) {
-      CreateAndConfigureStream(DemuxerStream::AUDIO);
+      CreateAndConfigureStream(DemuxerStream::AUDIO, setup_observers);
       ASSERT_TRUE(audio_);
       EXPECT_TRUE(frame_processor_->AddTrack(audio_id_, audio_.get()));
       SeekStream(audio_.get(), Milliseconds(0));
     }
     if (has_video) {
-      CreateAndConfigureStream(DemuxerStream::VIDEO);
+      CreateAndConfigureStream(DemuxerStream::VIDEO, setup_observers);
       ASSERT_TRUE(video_);
       EXPECT_TRUE(frame_processor_->AddTrack(video_id_, video_.get()));
       SeekStream(video_.get(), Milliseconds(0));
@@ -341,8 +354,11 @@
     last_read_buffer_ = buffer;
   }
 
-  void CreateAndConfigureStream(DemuxerStream::Type type) {
+  void CreateAndConfigureStream(DemuxerStream::Type type,
+                                bool setup_observers) {
     // TODO(wolenetz/dalecurtis): Also test with splicing disabled?
+
+    ChunkDemuxerStream* stream;
     switch (type) {
       case DemuxerStream::AUDIO: {
         ASSERT_FALSE(audio_);
@@ -354,6 +370,8 @@
         frame_processor_->OnPossibleAudioConfigUpdate(decoder_config);
         ASSERT_TRUE(
             audio_->UpdateAudioConfig(decoder_config, false, &media_log_));
+
+        stream = audio_.get();
         break;
       }
       case DemuxerStream::VIDEO: {
@@ -362,6 +380,7 @@
             new ChunkDemuxerStream(DemuxerStream::VIDEO, "2", range_api_));
         ASSERT_TRUE(video_->UpdateVideoConfig(TestVideoConfig::Normal(), false,
                                               &media_log_));
+        stream = video_.get();
         break;
       }
       // TODO(wolenetz): Test text coded frame processing.
@@ -370,6 +389,15 @@
         ASSERT_FALSE(true);
       }
     }
+
+    if (setup_observers) {
+      stream->set_append_observer_for_testing(
+          base::BindRepeating(&FrameProcessorTestCallbackHelper::OnAppend,
+                              base::Unretained(&callbacks_), type));
+      stream->set_group_start_observer_for_testing(
+          base::BindRepeating(&FrameProcessorTestCallbackHelper::OnGroupStart,
+                              base::Unretained(&callbacks_), type));
+    }
   }
 
   DISALLOW_COPY_AND_ASSIGN(FrameProcessorTest);
@@ -1335,7 +1363,7 @@
   }
 
   InSequence s;
-  AddTestTracks(HAS_VIDEO);
+  AddTestTracks(HAS_VIDEO | OBSERVE_APPENDS_AND_GROUP_STARTS);
   frame_processor_->SetSequenceMode(use_sequence_mode_);
 
   // Make the sequence mode buffering appear just like segments mode to simplify
@@ -1343,32 +1371,40 @@
   if (use_sequence_mode_)
     SetTimestampOffset(Milliseconds(1060));
 
+  // Note that the PTS of GOP non-keyframes earlier than the keyframe doesn't
+  // modify the GOP start of the buffered range here. This may change if we
+  // decide to improve spec for SAP Type 2 GOPs that begin a coded frame group.
+  EXPECT_CALL(callbacks_, OnGroupStart(DemuxerStream::VIDEO, DecodeTimestamp(),
+                                       Milliseconds(1060)));
   EXPECT_CALL(callbacks_,
               OnParseWarning(
                   SourceBufferParseWarning::kKeyframeTimeGreaterThanDependant));
   EXPECT_MEDIA_LOG(KeyframeTimeGreaterThanDependant("1.06", "1"));
+  EXPECT_CALL(callbacks_, OnAppend(DemuxerStream::VIDEO, _));
   EXPECT_CALL(callbacks_, PossibleDurationIncrease(Milliseconds(1070)));
   EXPECT_TRUE(ProcessFrames(
       "", "1060|0K 1000|10 1050|20 1010|30 1040|40 1020|50 1030|60"));
   EXPECT_EQ(Milliseconds(0), timestamp_offset_);
-
-  // Note that the PTS of GOP non-keyframes earlier than the keyframe doesn't
-  // modify the GOP start of the buffered range here. This may change if we
-  // decide to improve spec for SAP Type 2 GOPs that begin a coded frame group.
   CheckExpectedRangesByTimestamp(video_.get(), "{ [1060,1070) }");
 
   // Process just the keyframe of the next SAP Type 2 GOP in decode continuity
   // with the previous one.
+  // Note that this second GOP is buffered continuous with the first because
+  // there is no decode discontinuity detected. This results in inclusion of
+  // the significant PTS jump forward in the same continuous range.
+  EXPECT_CALL(
+      callbacks_,
+      OnGroupStart(DemuxerStream::VIDEO,
+                   DecodeTimestamp::FromPresentationTime(Milliseconds(60)),
+                   Milliseconds(1070)));
+  EXPECT_CALL(callbacks_, OnAppend(DemuxerStream::VIDEO, _));
   EXPECT_CALL(callbacks_, PossibleDurationIncrease(Milliseconds(1140)));
   EXPECT_TRUE(ProcessFrames("", "1130|70K"));
   EXPECT_EQ(Milliseconds(0), timestamp_offset_);
-
-  // Note that the second GOP is buffered continuous with the first because
-  // there was no decode discontinuity detected. This results in inclusion of
-  // the significant PTS jump forward in the same continuous range.
   CheckExpectedRangesByTimestamp(video_.get(), "{ [1060,1140) }");
 
   // Process the remainder of the second GOP.
+  EXPECT_CALL(callbacks_, OnAppend(DemuxerStream::VIDEO, _));
   EXPECT_CALL(callbacks_, PossibleDurationIncrease(Milliseconds(1140)));
   EXPECT_TRUE(
       ProcessFrames("", "1070|80 1120|90 1080|100 1110|110 1090|120 1100|130"));
@@ -1397,7 +1433,7 @@
   }
 
   InSequence s;
-  AddTestTracks(HAS_VIDEO);
+  AddTestTracks(HAS_VIDEO | OBSERVE_APPENDS_AND_GROUP_STARTS);
   frame_processor_->SetSequenceMode(use_sequence_mode_);
 
   // Make the sequence mode buffering appear just like segments mode to simplify
@@ -1405,10 +1441,19 @@
   if (use_sequence_mode_)
     SetTimestampOffset(Milliseconds(100));
 
+  EXPECT_CALL(callbacks_, OnGroupStart(DemuxerStream::VIDEO, DecodeTimestamp(),
+                                       Milliseconds(100)));
+  EXPECT_CALL(callbacks_, OnAppend(DemuxerStream::VIDEO, _));
   EXPECT_CALL(callbacks_, PossibleDurationIncrease(Milliseconds(140)));
   EXPECT_TRUE(ProcessFrames("", "100|0K 110|10 120|20 130|30"));
   EXPECT_EQ(Milliseconds(0), timestamp_offset_);
 
+  EXPECT_CALL(
+      callbacks_,
+      OnGroupStart(DemuxerStream::VIDEO,
+                   DecodeTimestamp::FromPresentationTime(Milliseconds(30)),
+                   Milliseconds(125)));
+  EXPECT_CALL(callbacks_, OnAppend(DemuxerStream::VIDEO, _));
   EXPECT_CALL(callbacks_, PossibleDurationIncrease(Milliseconds(165)));
   EXPECT_TRUE(ProcessFrames("", "125|40K 135|50 145|60 155|70"));
   EXPECT_EQ(Milliseconds(0), timestamp_offset_);
@@ -1429,7 +1474,7 @@
   }
 
   InSequence s;
-  AddTestTracks(HAS_VIDEO);
+  AddTestTracks(HAS_VIDEO | OBSERVE_APPENDS_AND_GROUP_STARTS);
   frame_processor_->SetSequenceMode(use_sequence_mode_);
 
   // Make the sequence mode buffering appear just like segments mode to simplify
@@ -1437,10 +1482,19 @@
   if (use_sequence_mode_)
     SetTimestampOffset(Milliseconds(100));
 
+  EXPECT_CALL(callbacks_, OnGroupStart(DemuxerStream::VIDEO, DecodeTimestamp(),
+                                       Milliseconds(100)));
+  EXPECT_CALL(callbacks_, OnAppend(DemuxerStream::VIDEO, _));
   EXPECT_CALL(callbacks_, PossibleDurationIncrease(Milliseconds(140)));
   EXPECT_TRUE(ProcessFrames("", "100|0K 110|10 120|20K 130|30"));
   EXPECT_EQ(Milliseconds(0), timestamp_offset_);
 
+  EXPECT_CALL(
+      callbacks_,
+      OnGroupStart(DemuxerStream::VIDEO,
+                   DecodeTimestamp::FromPresentationTime(Milliseconds(30)),
+                   Milliseconds(115)));
+  EXPECT_CALL(callbacks_, OnAppend(DemuxerStream::VIDEO, _));
   // TODO(wolenetz): Duration shouldn't be allowed to possibly increase to 140ms
   // here. See https://crbug.com/763620.
   EXPECT_CALL(callbacks_, PossibleDurationIncrease(Milliseconds(140)));
@@ -1463,7 +1517,7 @@
   }
 
   InSequence s;
-  AddTestTracks(HAS_VIDEO);
+  AddTestTracks(HAS_VIDEO | OBSERVE_APPENDS_AND_GROUP_STARTS);
   frame_processor_->SetSequenceMode(use_sequence_mode_);
 
   // Make the sequence mode buffering appear just like segments mode to simplify
@@ -1471,14 +1525,21 @@
   if (use_sequence_mode_)
     SetTimestampOffset(Milliseconds(120));
 
+  EXPECT_CALL(callbacks_, OnGroupStart(DemuxerStream::VIDEO, DecodeTimestamp(),
+                                       Milliseconds(120)));
   EXPECT_CALL(callbacks_,
               OnParseWarning(
                   SourceBufferParseWarning::kKeyframeTimeGreaterThanDependant));
   EXPECT_MEDIA_LOG(KeyframeTimeGreaterThanDependant("0.12", "0.1"));
+  EXPECT_CALL(callbacks_, OnAppend(DemuxerStream::VIDEO, _));
   EXPECT_CALL(callbacks_, PossibleDurationIncrease(Milliseconds(140)));
   EXPECT_TRUE(ProcessFrames("", "120|0K 100|10 130|20 110|30"));
   EXPECT_EQ(Milliseconds(0), timestamp_offset_);
 
+  // Note, we *don't* expect another OnGroupStart during the next ProcessFrames,
+  // since the next GOP's keyframe PTS is after the first GOP and close enough
+  // to be assured adjacent.
+  EXPECT_CALL(callbacks_, OnAppend(DemuxerStream::VIDEO, _));
   EXPECT_CALL(callbacks_, PossibleDurationIncrease(Milliseconds(165)));
   EXPECT_TRUE(ProcessFrames("", "145|40K 125|50 155|60 135|70"));
   EXPECT_EQ(Milliseconds(0), timestamp_offset_);
@@ -1503,7 +1564,7 @@
   }
 
   InSequence s;
-  AddTestTracks(HAS_VIDEO);
+  AddTestTracks(HAS_VIDEO | OBSERVE_APPENDS_AND_GROUP_STARTS);
   frame_processor_->SetSequenceMode(use_sequence_mode_);
 
   // Make the sequence mode buffering appear just like segments mode to simplify
@@ -1511,15 +1572,33 @@
   if (use_sequence_mode_)
     SetTimestampOffset(Milliseconds(120));
 
+  EXPECT_CALL(callbacks_, OnGroupStart(DemuxerStream::VIDEO, DecodeTimestamp(),
+                                       Milliseconds(120)));
   EXPECT_CALL(callbacks_,
               OnParseWarning(
                   SourceBufferParseWarning::kKeyframeTimeGreaterThanDependant));
   EXPECT_MEDIA_LOG(KeyframeTimeGreaterThanDependant("0.12", "0.1"));
+  EXPECT_CALL(callbacks_, OnAppend(DemuxerStream::VIDEO, _));
+  // There is a second GOP that is SAP-Type-2 within this first ProcessFrames,
+  // with PTS jumping forward far enough to trigger group start signalling and a
+  // flush.
+  EXPECT_CALL(
+      callbacks_,
+      OnGroupStart(DemuxerStream::VIDEO,
+                   DecodeTimestamp::FromPresentationTime(Milliseconds(30)),
+                   Milliseconds(140)));
+  EXPECT_CALL(callbacks_, OnAppend(DemuxerStream::VIDEO, _));
   EXPECT_CALL(callbacks_, PossibleDurationIncrease(Milliseconds(180)));
   EXPECT_TRUE(ProcessFrames(
       "", "120|0K 100|10 130|20 110|30 160|40K 140|50 170|60 150|70"));
   EXPECT_EQ(Milliseconds(0), timestamp_offset_);
 
+  EXPECT_CALL(
+      callbacks_,
+      OnGroupStart(DemuxerStream::VIDEO,
+                   DecodeTimestamp::FromPresentationTime(Milliseconds(70)),
+                   Milliseconds(155)));
+  EXPECT_CALL(callbacks_, OnAppend(DemuxerStream::VIDEO, _));
   // TODO(wolenetz): Duration shouldn't be allowed to possibly increase to 180ms
   // here. See https://crbug.com/763620.
   EXPECT_CALL(callbacks_, PossibleDurationIncrease(Milliseconds(180)));
@@ -1553,7 +1632,7 @@
   }
 
   InSequence s;
-  AddTestTracks(HAS_VIDEO);
+  AddTestTracks(HAS_VIDEO | OBSERVE_APPENDS_AND_GROUP_STARTS);
   frame_processor_->SetSequenceMode(use_sequence_mode_);
 
   // Make the sequence mode buffering appear just like segments mode to simplify
@@ -1561,11 +1640,20 @@
   if (use_sequence_mode_)
     SetTimestampOffset(Milliseconds(200));
 
+  EXPECT_CALL(callbacks_, OnGroupStart(DemuxerStream::VIDEO, DecodeTimestamp(),
+                                       Milliseconds(200)));
+  EXPECT_CALL(callbacks_, OnAppend(DemuxerStream::VIDEO, _));
   EXPECT_CALL(callbacks_, PossibleDurationIncrease(Milliseconds(240)));
   EXPECT_TRUE(ProcessFrames("", "200|0K 210|10 220|20 230|30"));
   EXPECT_EQ(Milliseconds(0), timestamp_offset_);
   CheckExpectedRangesByTimestamp(video_.get(), "{ [200,240) }");
 
+  EXPECT_CALL(
+      callbacks_,
+      OnGroupStart(DemuxerStream::VIDEO,
+                   DecodeTimestamp::FromPresentationTime(Milliseconds(30)),
+                   Milliseconds(100)));
+  EXPECT_CALL(callbacks_, OnAppend(DemuxerStream::VIDEO, _));
   // TODO(wolenetz): Duration shouldn't be allowed to possibly increase to 240ms
   // here. See https://crbug.com/763620.
   EXPECT_CALL(callbacks_, PossibleDurationIncrease(Milliseconds(240)));
@@ -1573,6 +1661,12 @@
   EXPECT_EQ(Milliseconds(0), timestamp_offset_);
   CheckExpectedRangesByTimestamp(video_.get(), "{ [100,140) [200,240) }");
 
+  EXPECT_CALL(
+      callbacks_,
+      OnGroupStart(DemuxerStream::VIDEO,
+                   DecodeTimestamp::FromPresentationTime(Milliseconds(70)),
+                   Milliseconds(140)));
+  EXPECT_CALL(callbacks_, OnAppend(DemuxerStream::VIDEO, _));
   EXPECT_CALL(callbacks_, PossibleDurationIncrease(Milliseconds(260)));
   EXPECT_TRUE(ProcessFrames("", "240|80K 250|90"));
   EXPECT_EQ(Milliseconds(0), timestamp_offset_);
@@ -1592,7 +1686,7 @@
   // the append sequence is required to have monotonically increasing DTS (even
   // across GOPs).
   InSequence s;
-  AddTestTracks(HAS_VIDEO);
+  AddTestTracks(HAS_VIDEO | OBSERVE_APPENDS_AND_GROUP_STARTS);
   frame_processor_->SetSequenceMode(use_sequence_mode_);
 
   // Make the sequence mode buffering appear just like segments mode to simplify
@@ -1600,14 +1694,25 @@
   if (use_sequence_mode_)
     SetTimestampOffset(Milliseconds(200));
 
+  EXPECT_CALL(
+      callbacks_,
+      OnGroupStart(DemuxerStream::VIDEO,
+                   DecodeTimestamp::FromPresentationTime(Milliseconds(200)),
+                   Milliseconds(200)));
+  EXPECT_CALL(callbacks_, OnAppend(DemuxerStream::VIDEO, _));
   EXPECT_CALL(callbacks_, PossibleDurationIncrease(Milliseconds(240)));
   EXPECT_TRUE(ProcessFrames("", "200K 210 220 230"));
   EXPECT_EQ(Milliseconds(0), timestamp_offset_);
   CheckExpectedRangesByTimestamp(video_.get(), "{ [200,240) }");
 
+  EXPECT_CALL(
+      callbacks_,
+      OnGroupStart(DemuxerStream::VIDEO,
+                   DecodeTimestamp::FromPresentationTime(Milliseconds(225)),
+                   Milliseconds(240)));
+  EXPECT_CALL(callbacks_, OnAppend(DemuxerStream::VIDEO, _));
   // Note that duration is reported based on PTS regardless of buffering model.
   EXPECT_CALL(callbacks_, PossibleDurationIncrease(Milliseconds(280)));
-
   // Append a second GOP whose first DTS is below the last DTS of the first GOP,
   // but whose PTS interval is continuous with the end of the first GOP.
   EXPECT_TRUE(ProcessFrames("", "240|225K 250|235 260|245 270|255"));
@@ -1625,6 +1730,108 @@
   }
 }
 
+TEST_P(FrameProcessorTest, OnlyKeyframes_ContinuousDts_ContinousPts_1) {
+  // Verifies that precisely one group start and one stream append occurs for a
+  // single continuous set of frames.
+  InSequence s;
+  AddTestTracks(HAS_AUDIO | OBSERVE_APPENDS_AND_GROUP_STARTS);
+  if (use_sequence_mode_)
+    frame_processor_->SetSequenceMode(true);
+
+  // Default test frame duration is 10 milliseconds.
+
+  EXPECT_CALL(callbacks_, OnGroupStart(DemuxerStream::AUDIO, DecodeTimestamp(),
+                                       base::TimeDelta()));
+  EXPECT_CALL(callbacks_, OnAppend(DemuxerStream::AUDIO, _));
+  EXPECT_CALL(callbacks_, PossibleDurationIncrease(Milliseconds(40)));
+  EXPECT_TRUE(ProcessFrames("0K 10K 20K 30K", ""));
+  EXPECT_EQ(Milliseconds(0), timestamp_offset_);
+
+  CheckExpectedRangesByTimestamp(audio_.get(), "{ [0,40) }");
+  CheckReadsThenReadStalls(audio_.get(), "0 10 20 30");
+}
+
+TEST_P(FrameProcessorTest, OnlyKeyframes_ContinuousDts_ContinuousPts_2) {
+  // Verifies that precisely one group start and one stream append occurs while
+  // processing a single continuous set of frames that uses fudge room to just
+  // barely remain adjacent.
+  InSequence s;
+  AddTestTracks(HAS_AUDIO | OBSERVE_APPENDS_AND_GROUP_STARTS);
+  if (use_sequence_mode_)
+    frame_processor_->SetSequenceMode(true);
+
+  frame_duration_ = Milliseconds(5);
+
+  EXPECT_CALL(callbacks_, OnGroupStart(DemuxerStream::AUDIO, DecodeTimestamp(),
+                                       base::TimeDelta()));
+  EXPECT_CALL(callbacks_, OnAppend(DemuxerStream::AUDIO, _));
+  EXPECT_CALL(callbacks_, PossibleDurationIncrease(Milliseconds(35)));
+  EXPECT_TRUE(ProcessFrames("0K 10K 20K 30K", ""));
+  EXPECT_EQ(Milliseconds(0), timestamp_offset_);
+
+  CheckExpectedRangesByTimestamp(audio_.get(), "{ [0,35) }");
+  CheckReadsThenReadStalls(audio_.get(), "0 10 20 30");
+}
+
+TEST_P(FrameProcessorTest,
+       OnlyKeyframes_ContinuousDts_DiscontinuousPtsJustBeyondFudgeRoom) {
+  // Verifies that, in ByPts, multiple group starts and distinct appends occur
+  // when processing a single DTS-continuous set of frames with PTS deltas that
+  // just barely exceed the adjacency assumption in FrameProcessor.
+  // Verifies that, in ByDts, precisely one group start and one stream append
+  // occur.
+  InSequence s;
+  AddTestTracks(HAS_AUDIO | OBSERVE_APPENDS_AND_GROUP_STARTS);
+  if (use_sequence_mode_)
+    frame_processor_->SetSequenceMode(true);
+
+  frame_duration_ = base::TimeDelta::FromMicroseconds(4999);
+
+  EXPECT_CALL(callbacks_, OnGroupStart(DemuxerStream::AUDIO, DecodeTimestamp(),
+                                       base::TimeDelta()));
+  EXPECT_CALL(callbacks_, OnAppend(DemuxerStream::AUDIO, _));
+  if (range_api_ == ChunkDemuxerStream::RangeApi::kNewByPts) {
+    // Frame "10|5K" following "0K" triggers start of new group and eventual
+    // append.
+    EXPECT_CALL(callbacks_, OnGroupStart(DemuxerStream::AUDIO,
+                                         DecodeTimestamp(), frame_duration_));
+    EXPECT_CALL(callbacks_, OnAppend(DemuxerStream::AUDIO, _));
+
+    // Frame "20|10K" following "10|5K" triggers start of new group and eventual
+    // append.
+    EXPECT_CALL(
+        callbacks_,
+        OnGroupStart(DemuxerStream::AUDIO,
+                     DecodeTimestamp::FromPresentationTime(Milliseconds(5)),
+                     Milliseconds(10) + frame_duration_));
+    EXPECT_CALL(callbacks_, OnAppend(DemuxerStream::AUDIO, _));
+
+    // Frame "30|15K" following "20|10K" triggers start of new group and
+    // eventual append.
+    EXPECT_CALL(
+        callbacks_,
+        OnGroupStart(DemuxerStream::AUDIO,
+                     DecodeTimestamp::FromPresentationTime(Milliseconds(10)),
+                     Milliseconds(20) + frame_duration_));
+    EXPECT_CALL(callbacks_, OnAppend(DemuxerStream::AUDIO, _));
+  }
+  EXPECT_CALL(callbacks_, PossibleDurationIncrease(
+                              base::TimeDelta::FromMicroseconds(34999)));
+  EXPECT_TRUE(ProcessFrames("0K 10|5K 20|10K 30|15K", ""));
+  EXPECT_EQ(Milliseconds(0), timestamp_offset_);
+
+  if (range_api_ == ChunkDemuxerStream::RangeApi::kNewByPts) {
+    // Note that the ByPts result is still buffered continuous since DTS was
+    // continuous and PTS was monotonically increasing (such that each group
+    // start was signalled by FrameProcessor to be continuous with the end of
+    // the previous group, if any.)
+    CheckExpectedRangesByTimestamp(audio_.get(), "{ [0,34) }");
+  } else {
+    CheckExpectedRangesByTimestamp(audio_.get(), "{ [0,19) }");
+  }
+  CheckReadsThenReadStalls(audio_.get(), "0 10 20 30");
+}
+
 INSTANTIATE_TEST_CASE_P(SequenceModeLegacyByDts,
                         FrameProcessorTest,
                         Values(FrameProcessorTestParams(