chromium/media/filters/hls_rendition_impl_unittest.cc

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "media/filters/hls_rendition_impl.h"

#include <string_view>

#include "base/test/gmock_callback_support.h"
#include "base/test/task_environment.h"
#include "media/base/test_helpers.h"
#include "media/filters/hls_network_access_impl.h"
#include "media/filters/hls_test_helpers.h"

namespace media {

namespace {

constexpr char kInitialFetchPlaylist[] =
    "#EXTM3U\n"
    "#EXT-X-VERSION:3\n"
    "#EXT-X-TARGETDURATION:2\n"
    "#EXT-X-MEDIA-SEQUENCE:14551245\n"
    "#EXTINF:2.00000,\n"
    "playlist_4500Kb_14551245.ts\n"
    "#EXTINF:2.00000,\n"
    "playlist_4500Kb_14551246.ts\n"
    "#EXTINF:2.00000,\n"
    "playlist_4500Kb_14551247.ts\n"
    "#EXTINF:2.00000,\n"
    "playlist_4500Kb_14551248.ts\n"
    "#EXTINF:2.00000,\n"
    "playlist_4500Kb_14551249.ts\n"
    "#EXTINF:2.00000,\n"
    "playlist_4500Kb_14551250.ts\n"
    "#EXTINF:2.00000,\n"
    "playlist_4500Kb_14551251.ts\n"
    "#EXTINF:2.00000,\n"
    "playlist_4500Kb_14551252.ts\n"
    "#EXTINF:2.00000,\n"
    "playlist_4500Kb_14551253.ts\n"
    "#EXTINF:2.00000,\n"
    "playlist_4500Kb_14551254.ts\n";

const std::string kSecondFetchLivePlaylist =
    "#EXTM3U\n"
    "#EXT-X-VERSION:3\n"
    "#EXT-X-TARGETDURATION:2\n"
    "#EXT-X-MEDIA-SEQUENCE:14551349\n"
    "#EXTINF:2.00000,\n"
    "playlist_4500Kb_14551349.ts\n"
    "#EXTINF:2.00000,\n"
    "playlist_4500Kb_14551350.ts\n"
    "#EXTINF:2.00000,\n"
    "playlist_4500Kb_14551351.ts\n"
    "#EXTINF:2.00000,\n"
    "playlist_4500Kb_14551352.ts\n"
    "#EXTINF:2.00000,\n"
    "playlist_4500Kb_14551353.ts\n"
    "#EXTINF:2.00000,\n"
    "playlist_4500Kb_14551354.ts\n"
    "#EXTINF:2.00000,\n"
    "playlist_4500Kb_14551355.ts\n"
    "#EXTINF:2.00000,\n"
    "playlist_4500Kb_14551356.ts\n"
    "#EXTINF:2.00000,\n"
    "playlist_4500Kb_14551357.ts\n"
    "#EXTINF:2.00000,\n"
    "playlist_4500Kb_14551358.ts\n";

const std::string kAESContent =
    "#EXTM3U\n"
    "#EXT-X-VERSION:3\n"
    "#EXT-X-TARGETDURATION:2\n"
    "#EXT-X-MEDIA-SEQUENCE:0\n"
    "#EXT-X-MEDIA-PLAYLIST-TYPE:VOD\n"
    "#EXT-X-KEY:METHOD=AES-128,URI=\"key1.enc\",IV="
    "0x66666666666666666666666666666666\n"
    "#EXTINF:2.00000,\n"
    "media_0.ts\n"
    "#EXTINF:2.00000,\n"
    "media_1.ts\n"
    "#EXT-X-KEY:METHOD=AES-128,URI=\"key2.enc\",IV="
    "0x67676767676767676767676767676767\n"
    "#EXTINF:2.00000,\n"
    "media_2.ts\n"
    "#EXTINF:2.00000,\n"
    "media_3.ts\n"
    "#EXTINF:2.00000,\n"
    "media_4.ts\n"
    "#EXTINF:2.00000,\n"
    "media_5.ts\n"
    "#EXT-X-ENDLIST\n";

const std::string kAESContentReplacement =
    "#EXTM3U\n"
    "#EXT-X-VERSION:3\n"
    "#EXT-X-TARGETDURATION:2\n"
    "#EXT-X-MEDIA-SEQUENCE:0\n"
    "#EXT-X-MEDIA-PLAYLIST-TYPE:VOD\n"
    "#EXT-X-KEY:METHOD=AES-128,URI=\"keyx1.enc\",IV="
    "0x66666666666666666666666666666666\n"
    "#EXTINF:2.00000,\n"
    "mediax_0.ts\n"
    "#EXTINF:2.00000,\n"
    "mediax_1.ts\n"
    "#EXT-X-KEY:METHOD=AES-128,URI=\"keyx2.enc\",IV="
    "0x68686868686868686868686868686868\n"
    "#EXTINF:2.00000,\n"
    "mediax_2.ts\n"
    "#EXTINF:2.00000,\n"
    "mediax_3.ts\n"
    "#EXTINF:2.00000,\n"
    "mediax_4.ts\n"
    "#EXTINF:2.00000,\n"
    "mediax_5.ts\n"
    "#EXT-X-ENDLIST\n";

const std::string kDiscontinuous =
    "#EXTM3U\n"
    "#EXT-X-VERSION:3\n"
    "#EXT-X-TARGETDURATION:2\n"
    "#EXT-X-MEDIA-SEQUENCE:0\n"
    "#EXT-X-PLAYLIST-TYPE:VOD\n"
    "#EXT-X-INDEPENDENT-SEGMENTS\n"
    "#EXTINF:2.000000,\n"
    "bip00.ts\n"
    "#EXTINF:2.000000,\n"
    "bip01.ts\n"
    "#EXTINF:2.000000,\n"
    "bip02.ts\n"
    "#EXT-X-DISCONTINUITY\n"
    "#EXTINF:1.600000,\n"
    "data00.ts\n"
    "#EXTINF:1.600000,\n"
    "data01.ts\n"
    "#EXTINF:1.600000,\n"
    "data02.ts\n"
    "#EXTINF:1.600000,\n"
    "data03.ts\n"
    "#EXT-X-ENDLIST\n";

}  // namespace

using testing::_;
using testing::ElementsAreArray;
using testing::Return;

MATCHER_P(MediaSegmentHasUrl, urlstr, "MediaSegment has provided URL") {
  return arg.GetUri() == GURL(urlstr);
}

class HlsRenditionImplUnittest : public testing::Test {
 protected:
  std::unique_ptr<MockManifestDemuxerEngineHost> mock_mdeh_;
  std::unique_ptr<MockHlsRenditionHost> mock_hrh_;
  base::test::TaskEnvironment task_environment_{
      base::test::TaskEnvironment::TimeSource::MOCK_TIME};

  std::unique_ptr<HlsRenditionImpl> MakeVodRendition(std::string_view content) {
    constexpr hls::types::DecimalInteger version = 3;
    auto uri = GURL("https://example.com/manifest.m3u8");
    auto parsed = hls::MediaPlaylist::Parse(content, uri, version, nullptr);
    if (!parsed.has_value()) {
      LOG(ERROR) << MediaSerialize(std::move(parsed).error());
      return nullptr;
    }
    auto playlist = std::move(parsed).value();
    auto duration = playlist->GetComputedDuration();
    return std::make_unique<HlsRenditionImpl>(mock_mdeh_.get(), mock_hrh_.get(),
                                              "test", std::move(playlist),
                                              duration, uri);
  }

  std::unique_ptr<HlsRenditionImpl> MakeLiveRendition(
      GURL uri,
      std::string_view content) {
    constexpr hls::types::DecimalInteger version = 3;
    auto parsed = hls::MediaPlaylist::Parse(content, uri, version, nullptr);
    if (!parsed.has_value()) {
      LOG(ERROR) << MediaSerialize(std::move(parsed).error());
      return nullptr;
    }
    return std::make_unique<HlsRenditionImpl>(mock_mdeh_.get(), mock_hrh_.get(),
                                              "test", std::move(parsed).value(),
                                              std::nullopt, uri);
  }

  MOCK_METHOD(void, CheckStateComplete, (base::TimeDelta delay), ());

  ManifestDemuxer::DelayCallback BindCheckState(base::TimeDelta time) {
    EXPECT_CALL(*this, CheckStateComplete(time));
    return base::BindOnce(&HlsRenditionImplUnittest::CheckStateComplete,
                          base::Unretained(this));
  }

  ManifestDemuxer::DelayCallback BindCheck0Sec() {
    EXPECT_CALL(*this, CheckStateComplete(base::Seconds(0)));
    return base::BindOnce(&HlsRenditionImplUnittest::CheckStateComplete,
                          base::Unretained(this));
  }

  ManifestDemuxer::DelayCallback BindCheckStateNoExpect() {
    return base::BindOnce(&HlsRenditionImplUnittest::CheckStateComplete,
                          base::Unretained(this));
  }

  void RequireAppend(base::span<const uint8_t> data, bool return_value = true) {
    EXPECT_CALL(*mock_mdeh_,
                AppendAndParseData(_, _, _, base::as_byte_span(data)))
        .WillOnce(Return(return_value));
  }

  void RespondWithRange(base::TimeDelta start, base::TimeDelta end) {
    Ranges<base::TimeDelta> ranges;
    if (start != end) {
      ranges.Add(start, end);
    }
    EXPECT_CALL(*mock_mdeh_, GetBufferedRanges("test"))
        .WillOnce(Return(ranges));
  }

  void RespondWithRangeTwice(base::TimeDelta A,
                             base::TimeDelta B,
                             base::TimeDelta X,
                             base::TimeDelta Y) {
    Ranges<base::TimeDelta> ab;
    if (A != B) {
      ab.Add(A, B);
    }
    Ranges<base::TimeDelta> xy;
    if (X != Y) {
      xy.Add(X, Y);
    }
    EXPECT_CALL(*mock_mdeh_, GetBufferedRanges("test"))
        .WillOnce(Return(ab))
        .WillOnce(Return(xy));
  }

  void SupplyAndExpectJunkData(base::TimeDelta initial_response_start,
                               base::TimeDelta initial_response_end,
                               base::TimeDelta fetch_expected_time) {
    std::string junk_content = "abcdefg, I dont like to sing rhyming songs";
    EXPECT_CALL(*mock_hrh_, ReadMediaSegment(_, _, _, _))
        .WillOnce([content = junk_content, host = mock_hrh_.get()](
                      const hls::MediaSegment&, bool, bool,
                      HlsDataSourceProvider::ReadCb cb) {
          auto stream = StringHlsDataSourceStreamFactory::CreateStream(content);
          std::move(cb).Run(std::move(stream));
        });
    EXPECT_CALL(
        *mock_mdeh_,
        AppendAndParseData("test", _, _,
                           ElementsAreArray(base::as_byte_span(junk_content))))
        .WillOnce(Return(true));
    Ranges<base::TimeDelta> initial_range;
    Ranges<base::TimeDelta> appended_range;
    if (initial_response_end != initial_response_start) {
      initial_range.Add(initial_response_start, initial_response_end);
    }
    appended_range.Add(fetch_expected_time - base::Seconds(1),
                       fetch_expected_time + base::Seconds(1));
    EXPECT_CALL(*mock_mdeh_, GetBufferedRanges("test"))
        .Times(2)
        .WillOnce(Return(initial_range))
        .WillOnce(Return(appended_range));
  }

  void RespondToUrl(std::string uri, std::string content) {
    EXPECT_CALL(*mock_hrh_, UpdateNetworkSpeed(_));
    EXPECT_CALL(*mock_hrh_, ReadMediaSegment(MediaSegmentHasUrl(uri), _, _, _))
        .WillOnce([content = std::move(content), host = mock_hrh_.get()](
                      const hls::MediaSegment& segment, bool, bool,
                      HlsDataSourceProvider::ReadCb cb) {
          if (auto enc_data = segment.GetEncryptionData()) {
            ASSERT_FALSE(enc_data->NeedsKeyFetch());
          }
          auto stream = StringHlsDataSourceStreamFactory::CreateStream(content);
          std::move(cb).Run(std::move(stream));
        });
  }

  void InterceptEncDataOnFetch(
      std::string uri,
      std::string content,
      base::OnceCallback<void(hls::MediaSegment::EncryptionData*)> intercept) {
    EXPECT_CALL(*mock_hrh_, UpdateNetworkSpeed(_));
    EXPECT_CALL(*mock_hrh_, ReadMediaSegment(MediaSegmentHasUrl(uri), _, _, _))
        .WillOnce([content = std::move(content), host = mock_hrh_.get(),
                   intercept = std::move(intercept)](
                      const hls::MediaSegment& segment, bool, bool,
                      HlsDataSourceProvider::ReadCb cb) mutable {
          if (auto enc_data = segment.GetEncryptionData()) {
            ASSERT_TRUE(enc_data->NeedsKeyFetch());
            std::move(intercept).Run(enc_data.get());
          }
          auto stream = StringHlsDataSourceStreamFactory::CreateStream(content);
          std::move(cb).Run(std::move(stream));
        });
  }

  std::tuple<std::string, std::unique_ptr<crypto::SymmetricKey>> Encrypt(
      std::string cleartext,
      std::string ivstr) {
    std::string ciphertext;
    auto mode = crypto::SymmetricKey::AES;
    auto key = crypto::SymmetricKey::GenerateRandomKey(mode, 128);
    auto encryptor = std::make_unique<crypto::Encryptor>();
    encryptor->Init(key.get(), crypto::Encryptor::Mode::CBC, ivstr);
    encryptor->Encrypt(cleartext, &ciphertext);
    return std::make_tuple(ciphertext, std::move(key));
  }

 public:
  HlsRenditionImplUnittest()
      : mock_mdeh_(std::make_unique<MockManifestDemuxerEngineHost>()),
        mock_hrh_(std::make_unique<MockHlsRenditionHost>()) {
    EXPECT_CALL(*mock_mdeh_, RemoveRole("test"));
  }
};

TEST_F(HlsRenditionImplUnittest, TestCheckStateFromNoData) {
  auto rendition = MakeVodRendition(kInitialFetchPlaylist);
  ASSERT_NE(rendition, nullptr);

  SupplyAndExpectJunkData(base::Seconds(0), base::Seconds(0), base::Seconds(1));
  rendition->CheckState(base::Seconds(0), 1.0,
                        BindCheckState(base::Seconds(0)));

  task_environment_.RunUntilIdle();
}

TEST_F(HlsRenditionImplUnittest, TestCheckStateWithLargeBufferCached) {
  auto rendition = MakeVodRendition(kInitialFetchPlaylist);
  ASSERT_NE(rendition, nullptr);

  // Prime the download speed cache.
  SupplyAndExpectJunkData(base::Seconds(0), base::Seconds(0), base::Seconds(1));
  rendition->CheckState(base::Seconds(0), 1.0,
                        BindCheckState(base::Seconds(0)));
  task_environment_.RunUntilIdle();

  // This time respond with a large range of loaded data.
  // Time until underflow is going to be 12 seconds here - the fetch time
  // average is zero, since this is a unittest, and we subtract 5 seconds flag
  // giving a delay of 7 seconds.
  RespondWithRange(base::Seconds(0), base::Seconds(12));
  rendition->CheckState(base::Seconds(0), 1.0,
                        BindCheckState(base::Seconds(7)));

  task_environment_.RunUntilIdle();
}

TEST_F(HlsRenditionImplUnittest, TestCheckStateWithTooLateBuffer) {
  auto rendition = MakeVodRendition(kInitialFetchPlaylist);
  ASSERT_NE(rendition, nullptr);

  RespondWithRange(base::Seconds(10), base::Seconds(12));
  EXPECT_CALL(*mock_mdeh_, OnError(_));
  rendition->CheckState(base::Seconds(0), 1.0, BindCheckStateNoExpect());

  task_environment_.RunUntilIdle();
}

TEST_F(HlsRenditionImplUnittest, TestStop) {
  auto rendition = MakeVodRendition(kInitialFetchPlaylist);
  ASSERT_NE(rendition, nullptr);

  rendition->Stop();

  // Should always be kNoTimestamp after `Stop()` and no network requests.
  rendition->CheckState(base::Seconds(0), 1.0, BindCheckState(kNoTimestamp));
}

TEST_F(HlsRenditionImplUnittest, TestNonRealTimePlaybackRate) {
  auto rendition =
      MakeLiveRendition(GURL("http://example.com"), kInitialFetchPlaylist);
  ASSERT_NE(rendition, nullptr);
  ASSERT_EQ(rendition->GetDuration(), std::nullopt);

  // Any rate not 0.0 or 1.0 should error.
  EXPECT_CALL(*mock_mdeh_, OnError(_));
  rendition->CheckState(base::Seconds(0), 2.0, BindCheckStateNoExpect());
  task_environment_.RunUntilIdle();
}

TEST_F(HlsRenditionImplUnittest, TestCreateRenditionPaused) {
  auto rendition =
      MakeLiveRendition(GURL("http://example.com"), kInitialFetchPlaylist);
  ASSERT_NE(rendition, nullptr);
  ASSERT_EQ(rendition->GetDuration(), std::nullopt);

  // CheckState causes the rentidion to:
  // Check buffered ranges first
  RespondWithRangeTwice(base::Seconds(0), base::Seconds(0), base::Seconds(0),
                        base::Seconds(5));
  // The first segment will be queried
  std::string tscontent = "tscontent";
  RespondToUrl("http://example.com/playlist_4500Kb_14551245.ts", tscontent);
  // Then appended.
  EXPECT_CALL(*mock_mdeh_,
              AppendAndParseData(_, _, _, base::as_byte_span(tscontent)))
      .WillOnce(Return(true));
  // CheckState should in this case respond with a delay of zero seconds.
  rendition->CheckState(base::Seconds(0), 0.0,
                        BindCheckState(base::Seconds(0)));
  task_environment_.RunUntilIdle();
}

TEST_F(HlsRenditionImplUnittest, TestPausedRenditionHasSomeData) {
  auto rendition =
      MakeLiveRendition(GURL("http://example.com"), kInitialFetchPlaylist);
  ASSERT_NE(rendition, nullptr);
  ASSERT_EQ(rendition->GetDuration(), std::nullopt);

  // CheckState causes the rentidion to:
  // Check buffered ranges first. In this case, we've loaded a bunch of content
  // already, and our loaded ranges are [0 - 8)
  RespondWithRangeTwice(base::Seconds(0), base::Seconds(8), base::Seconds(0),
                        base::Seconds(16));

  // The next unqueried segment will be queried
  std::string tscontent = "tscontent";
  RespondToUrl("http://example.com/playlist_4500Kb_14551245.ts", tscontent);
  // Then appended.
  EXPECT_CALL(*mock_mdeh_,
              AppendAndParseData(_, _, _, base::as_byte_span(tscontent)))
      .WillOnce(Return(true));
  // CheckState should in this case respond with a delay of zero seconds.
  rendition->CheckState(base::Seconds(0), 0.0,
                        BindCheckState(base::Seconds(0)));
  task_environment_.RunUntilIdle();
}

TEST_F(HlsRenditionImplUnittest, TestPausedRenditionHasEnoughBufferedData) {
  auto rendition =
      MakeLiveRendition(GURL("http://example.com"), kInitialFetchPlaylist);
  ASSERT_NE(rendition, nullptr);
  ASSERT_EQ(rendition->GetDuration(), std::nullopt);

  // CheckState causes the rentidion to:
  // Check buffered ranges first. In this case, we've loaded a bunch of content
  // already, and our loaded ranges are [0 - 12)
  Ranges<base::TimeDelta> loaded_ranges;
  loaded_ranges.Add(base::Seconds(0), base::Seconds(12));
  EXPECT_CALL(*mock_mdeh_, GetBufferedRanges(_))
      .WillOnce(Return(loaded_ranges));
  // Old data will try to be removed. Since media time is 0, there is nothing
  // to do. Then there will be an attempt to fetch a new manifest, which won't
  // have any work to do either, instead just posting the delay_cb back.
  // CheckState should in this case respond with a delay of 12 - 10 / 2 seconds.
  rendition->CheckState(base::Seconds(0), 0.0,
                        BindCheckState(base::Seconds(7)));
  task_environment_.RunUntilIdle();
}

TEST_F(HlsRenditionImplUnittest, TestRenditionHasEnoughDataFetchNewManifest) {
  auto rendition =
      MakeLiveRendition(GURL("http://example.com"), kInitialFetchPlaylist);
  ASSERT_NE(rendition, nullptr);
  ASSERT_EQ(rendition->GetDuration(), std::nullopt);

  // CheckState causes the rentidion to:
  // Check buffered ranges first. In this case, we've loaded a bunch of content
  // already, and our loaded ranges are [0 - 12)
  Ranges<base::TimeDelta> loaded_ranges;
  loaded_ranges.Add(base::Seconds(0), base::Seconds(12));
  EXPECT_CALL(*mock_mdeh_, GetBufferedRanges(_))
      .WillOnce(Return(loaded_ranges));
  // Old data will try to be removed. Since media time is 0, there is nothing
  // to do. Then there will be an attempt to fetch a new manifest, which will
  // get an update.
  task_environment_.FastForwardBy(base::Seconds(23));
  EXPECT_CALL(*mock_hrh_,
              UpdateRenditionManifestUri("test", GURL("http://example.com"), _))
      .WillOnce(
          [](std::string role, GURL uri, base::OnceCallback<void(bool)> cb) {
            std::move(cb).Run(true);
          });

  // CheckState should in this case respond with a delay of 12 - 10/2 seconds.
  rendition->CheckState(base::Seconds(0), 0.0,
                        BindCheckState(base::Seconds(7)));
  task_environment_.RunUntilIdle();
}

TEST_F(HlsRenditionImplUnittest, TestRenditionHasEnoughDataDeleteOldContent) {
  auto rendition =
      MakeLiveRendition(GURL("http://example.com"), kInitialFetchPlaylist);
  ASSERT_NE(rendition, nullptr);
  ASSERT_EQ(rendition->GetDuration(), std::nullopt);

  // CheckState causes the rentidion to:
  // Check buffered ranges first. In this case, we've loaded a bunch of content
  // already, and our loaded ranges are [0 - 32)
  Ranges<base::TimeDelta> loaded_ranges;
  loaded_ranges.Add(base::Seconds(0), base::Seconds(32));
  EXPECT_CALL(*mock_mdeh_, GetBufferedRanges(_))
      .WillOnce(Return(loaded_ranges));
  // Old data will try to be removed. Since media time is 15, there are 5
  // seconds of old data to delete. There will be no new fetch and parse for
  // manifest updates.
  EXPECT_CALL(*mock_mdeh_, Remove(_, base::Seconds(0), base::Seconds(13)));
  task_environment_.FastForwardBy(base::Seconds(15));

  // CheckState should in this case respond with a delay of 17 - 10 / 2 seconds.
  rendition->CheckState(base::Seconds(15), 0.0,
                        BindCheckState(base::Seconds(12)));

  task_environment_.RunUntilIdle();
}

TEST_F(HlsRenditionImplUnittest, TestStopLive) {
  auto rendition =
      MakeLiveRendition(GURL("http://example.com"), kInitialFetchPlaylist);
  ASSERT_NE(rendition, nullptr);

  rendition->Stop();

  // Should always be kNoTimestamp after `Stop()` and no network requests.
  rendition->CheckState(base::Seconds(0), 1.0, BindCheckState(kNoTimestamp));
}

TEST_F(HlsRenditionImplUnittest, TestPauseAndUnpause) {
  auto rendition =
      MakeLiveRendition(GURL("http://example.com"), kInitialFetchPlaylist);
  ASSERT_NE(rendition, nullptr);
  ASSERT_EQ(rendition->GetDuration(), std::nullopt);

  ON_CALL(*mock_mdeh_, OnError(_)).WillByDefault([](PipelineStatus st) {
    LOG(ERROR) << MediaSerialize(st);
  });

  // CheckState will start with a paused player. It will query BufferedRanges
  // for the CheckState function, then try to fetch. This will pop the first
  // segment and try to load it. This will then get appended, and ranges will
  // be checked again. It will report 2 seconds of content, which contains
  // the media time (0.0), and so a response to check state again in 0 seconds
  // will happen.
  std::string tscontent = "tscontent";
  RespondToUrl("http://example.com/playlist_4500Kb_14551245.ts", tscontent);
  RequireAppend(base::as_byte_span(tscontent));
  RespondWithRangeTwice(base::Seconds(0), base::Seconds(0), base::Seconds(0),
                        base::Seconds(2));
  rendition->CheckState(base::Seconds(0), 0.0,
                        BindCheckState(base::Seconds(0)));
  task_environment_.RunUntilIdle();

  // After the init process finishes, lets pretend there are 32 seconds of data
  // in the buffer. A user presses play after 9 second of the video being
  // paused. Rate goes to 1, and the delta between now and the pause timestamp
  // is 9 seconds, which is well within the duration of the manifest. The
  // rendition impl will request a seek to 9 seconds, and return no timestamp.
  EXPECT_CALL(*mock_mdeh_, RequestSeek(base::Seconds(9)));
  task_environment_.FastForwardBy(base::Seconds(9));
  rendition->CheckState(base::Seconds(0), 1.0, BindCheckState(kNoTimestamp));
  task_environment_.RunUntilIdle();

  // After the pipeline does it's seeking shenanigans, another check state
  // event will be called at 9 seconds, rate 1.0. Because there are 23 seconds
  // now left in the buffer, the response will be a requested pause of 18
  // seconds, and old buffers (from 0 - 7 seconds) will be cleared.
  RespondWithRange(base::Seconds(0), base::Seconds(32));
  EXPECT_CALL(*mock_mdeh_, Remove(_, base::Seconds(0), base::Seconds(7)));
  rendition->CheckState(base::Seconds(9), 1.0,
                        BindCheckState(base::Seconds(18)));
  task_environment_.RunUntilIdle();

  // At 10 seconds the user will pause again, which will trigger another
  // state check. This will update the pause timestamp to 9 seconds, and because
  // we aren't in the initialization step, will return kNoTimestamp. Any other
  // state checks with a rate of 0 should also return no timestamp.
  rendition->CheckState(base::Seconds(10), 0.0, BindCheckState(kNoTimestamp));
  task_environment_.RunUntilIdle();
  rendition->CheckState(base::Seconds(10), 0.0, BindCheckState(kNoTimestamp));
  task_environment_.RunUntilIdle();
  rendition->CheckState(base::Seconds(10), 0.0, BindCheckState(kNoTimestamp));
  task_environment_.RunUntilIdle();

  // Now the user waits for 190 seconds. Media time hasn't moved, so a seek
  // will be required. The segment queue will be reset, with a new head time
  // of 10s (media_time) + 190s (paused duration). A seek will be requested,
  // then a manifest fetch will happen, then a response to CheckState should
  // come back with a 0 second delay.
  EXPECT_CALL(*mock_mdeh_, RequestSeek(base::Seconds(202)));
  EXPECT_CALL(*mock_hrh_, UpdateRenditionManifestUri("test", _, _))
      .WillOnce(base::test::RunOnceCallback<2>(true));
  task_environment_.FastForwardBy(base::Seconds(190));
  rendition->CheckState(base::Seconds(10), 1.0,
                        BindCheckState(base::Seconds(0)));
  task_environment_.RunUntilIdle();

  // We have to actually do what the UpdateRenditionManifestUri method does now,
  // which is to fetch the manifest and set it on the rendition.
  auto parsed = hls::MediaPlaylist::Parse(
      kSecondFetchLivePlaylist, GURL("http://example.com"), 3, nullptr);
  CHECK(parsed.has_value());
  rendition->UpdatePlaylist(std::move(parsed).value(), std::nullopt);

  // Once again, the pipeline finishes it's seeking, and a new media CheckState
  // event happens, this time for 200 seconds. Now the media_time is way past
  // the end of our buffered ranges, so we need data ASAP. We're live, segments
  // is not exhausted (it's just been updated!) and so we fetch the next one.
  // After appending and parsing, it's brought our loaded ranges up to 202, and
  // the response to check state is to run it again in 0 seconds.
  RespondWithRangeTwice(base::Seconds(0), base::Seconds(32), base::Seconds(0),
                        base::Seconds(202));
  std::string newcontent = "newcontent";
  RespondToUrl("http://example.com/playlist_4500Kb_14551349.ts", newcontent);
  RequireAppend(base::as_byte_span(newcontent));
  rendition->CheckState(base::Seconds(200), 1.0,
                        BindCheckState(base::Seconds(0)));
  task_environment_.RunUntilIdle();

  // this time, the ranges are only 2 seconds past media time, so more data is
  // requested again, this time for the next segment. Lets pretend that next
  // segment has 20 seconds of data in it, bringing new range end to 222. The
  // response will still be 0 seconds.
  RespondWithRangeTwice(base::Seconds(0), base::Seconds(202), base::Seconds(0),
                        base::Seconds(222));
  newcontent = "blah";
  RespondToUrl("http://example.com/playlist_4500Kb_14551350.ts", "blah");
  RequireAppend(base::as_byte_span(newcontent));
  rendition->CheckState(base::Seconds(200), 1.0,
                        BindCheckState(base::Seconds(0)));
  task_environment_.RunUntilIdle();

  // Now, finally, we've satisfied the buffer, so we can clear old segments,
  // and the loop can pause for (22 - 10/2) or 17 seconds.
  RespondWithRange(base::Seconds(0), base::Seconds(222));
  EXPECT_CALL(*mock_mdeh_, Remove(_, base::Seconds(0), base::Seconds(198)));
  rendition->CheckState(base::Seconds(200), 1.0,
                        BindCheckState(base::Seconds(17)));
  task_environment_.RunUntilIdle();
}

TEST_F(HlsRenditionImplUnittest, TestDiscontinuity) {
  auto rendition = MakeVodRendition(kDiscontinuous);
  ASSERT_NE(rendition, nullptr);
  const std::string content = "ehh, whatever";

  RespondWithRangeTwice(base::Seconds(0), base::Seconds(0), base::Seconds(0),
                        base::Seconds(2));
  RespondToUrl("https://example.com/bip00.ts", content);
  RequireAppend(base::as_byte_span(content));
  rendition->CheckState(base::Seconds(0), 0.0, BindCheck0Sec());
  task_environment_.RunUntilIdle();

  RespondWithRangeTwice(base::Seconds(0), base::Seconds(0), base::Seconds(0),
                        base::Seconds(2));
  RespondToUrl("https://example.com/bip01.ts", content);
  RequireAppend(base::as_byte_span(content));
  rendition->CheckState(base::Seconds(0), 0.0, BindCheck0Sec());
  task_environment_.RunUntilIdle();

  RespondWithRangeTwice(base::Seconds(0), base::Seconds(0), base::Seconds(0),
                        base::Seconds(2));
  RespondToUrl("https://example.com/bip02.ts", content);
  RequireAppend(base::as_byte_span(content));
  rendition->CheckState(base::Seconds(0), 0.0, BindCheck0Sec());
  task_environment_.RunUntilIdle();

  RespondWithRangeTwice(base::Seconds(0), base::Seconds(0), base::Seconds(0),
                        base::Seconds(2));
  RespondToUrl("https://example.com/data00.ts", content);

  EXPECT_CALL(*mock_mdeh_, ResetParserState("test", base::Seconds(8.6), _));

  RequireAppend(base::as_byte_span(content));
  rendition->CheckState(base::Seconds(0), 0.0, BindCheck0Sec());
  task_environment_.RunUntilIdle();

  RespondWithRangeTwice(base::Seconds(0), base::Seconds(0), base::Seconds(0),
                        base::Seconds(2));
  RespondToUrl("https://example.com/data01.ts", content);
  RequireAppend(base::as_byte_span(content));
  rendition->CheckState(base::Seconds(0), 0.0, BindCheck0Sec());
  task_environment_.RunUntilIdle();

  RespondWithRangeTwice(base::Seconds(0), base::Seconds(0), base::Seconds(0),
                        base::Seconds(2));
  RespondToUrl("https://example.com/data02.ts", content);
  RequireAppend(base::as_byte_span(content));
  rendition->CheckState(base::Seconds(0), 0.0, BindCheck0Sec());
  task_environment_.RunUntilIdle();
}

TEST_F(HlsRenditionImplUnittest, TestAES128Content) {
  auto rendition = MakeVodRendition(kAESContent);

  ASSERT_NE(rendition, nullptr);
  ASSERT_EQ(rendition->GetDuration(), base::Seconds(12));

  ON_CALL(*mock_mdeh_, OnError(_)).WillByDefault([](PipelineStatus st) {
    LOG(ERROR) << MediaSerialize(st);
  });

  std::string cleartext = "some kind of ts content.";
  std::string ciphertext;
  std::unique_ptr<crypto::SymmetricKey> key;
  std::tie(ciphertext, key) = Encrypt(cleartext, "ffffffffffffffff");

  /* START CHECK 1 */
  // CheckState will start the paused player, query BufferedRanges, get 0-0,
  // which will trigger an attempt to fetch.
  RespondWithRangeTwice(base::Seconds(0), base::Seconds(0), base::Seconds(0),
                        base::Seconds(2));

  // The fetch will request the segment at "media_0.ts", which has an encryption
  // data attached. We need to populate that here so we can use the same key to
  // encrypt our plaintext.
  InterceptEncDataOnFetch("https://example.com/media_0.ts", ciphertext,
                          base::BindOnce(
                              [](crypto::SymmetricKey* key,
                                 hls::MediaSegment::EncryptionData* enc_data) {
                                enc_data->ImportKey(key->key());
                              },
                              key.get()));

  // The cleartext will get appended, and the network speed will be updated
  // to some massive number.
  RequireAppend(base::as_byte_span(cleartext));

  // Ranges will be checked again, and we claim that it was data from 0-2.
  // Because our time (0) is within the range 0-2, CheckState will request a 0
  // second delay.
  rendition->CheckState(base::Seconds(0), 0.0,
                        BindCheckState(base::Seconds(0)));
  task_environment_.RunUntilIdle();

  /* START CHECK 2 */

  // On the second fetch, the encryption data should remain the same,
  // so we can just use new text.
  RespondWithRangeTwice(base::Seconds(0), base::Seconds(2), base::Seconds(0),
                        base::Seconds(4));
  RespondToUrl("https://example.com/media_1.ts", ciphertext);
  RequireAppend(base::as_byte_span(cleartext));
  rendition->CheckState(base::Seconds(0), 0.0,
                        BindCheckState(base::Seconds(0)));
  task_environment_.RunUntilIdle();

  /* START CHECK 3 */

  // There's a new key, and a new IV.
  std::string ciphertext2;
  std::tie(ciphertext2, key) = Encrypt(cleartext, "gggggggggggggggg");

  // CheckState will start the paused player, query BufferedRanges, get 0-4
  // which will trigger an attempt to fetch.
  RespondWithRangeTwice(base::Seconds(0), base::Seconds(4), base::Seconds(0),
                        base::Seconds(6));

  // The fetch will request the segment at "media_2.ts", which has a new
  // encryption data.
  InterceptEncDataOnFetch("https://example.com/media_2.ts", ciphertext2,
                          base::BindOnce(
                              [](crypto::SymmetricKey* key,
                                 hls::MediaSegment::EncryptionData* enc_data) {
                                enc_data->ImportKey(key->key());
                              },
                              key.get()));

  RequireAppend(base::as_byte_span(cleartext));
  rendition->CheckState(base::Seconds(0), 0.0,
                        BindCheckState(base::Seconds(0)));
  task_environment_.RunUntilIdle();

  /* START CHECK 4 */

  // Update the playlist. The segment stream should keep around media_3.ts,
  // but follow it up with mediax_4.ts
  auto parsed = hls::MediaPlaylist::Parse(
      kAESContentReplacement, GURL("https://example.com/manifest.m3u8"), 3,
      nullptr);
  CHECK(parsed.has_value());
  rendition->UpdatePlaylist(std::move(parsed).value(), std::nullopt);

  // The encryption data is the same for segment 3, since it didn't change.
  RespondWithRangeTwice(base::Seconds(0), base::Seconds(2), base::Seconds(0),
                        base::Seconds(4));
  RespondToUrl("https://example.com/media_3.ts", ciphertext2);
  RequireAppend(base::as_byte_span(cleartext));
  rendition->CheckState(base::Seconds(0), 0.0,
                        BindCheckState(base::Seconds(0)));
  task_environment_.RunUntilIdle();

  /* START CHECK 5 */

  // This time mediax_4 doesn't have a new encryption segment, but it does need
  // a key fetch.

  std::string ciphertext3;
  std::tie(ciphertext3, key) = Encrypt(cleartext, "hhhhhhhhhhhhhhhh");

  // CheckState will start the paused player, query BufferedRanges, get 0-4
  // which will trigger an attempt to fetch.
  RespondWithRangeTwice(base::Seconds(0), base::Seconds(4), base::Seconds(0),
                        base::Seconds(6));

  // The fetch will request the segment at "mediax_4.ts", for which the
  // encryption data has not been fetched.
  InterceptEncDataOnFetch("https://example.com/mediax_4.ts", ciphertext3,
                          base::BindOnce(
                              [](crypto::SymmetricKey* key,
                                 hls::MediaSegment::EncryptionData* enc_data) {
                                enc_data->ImportKey(key->key());
                              },
                              key.get()));
  RequireAppend(base::as_byte_span(cleartext));
  rendition->CheckState(base::Seconds(0), 0.0,
                        BindCheckState(base::Seconds(0)));
  task_environment_.RunUntilIdle();

  /* START CHECK 6 */

  // The encryption data is the same for segment 5, since it didn't change.
  RespondWithRangeTwice(base::Seconds(0), base::Seconds(2), base::Seconds(0),
                        base::Seconds(4));
  RespondToUrl("https://example.com/mediax_5.ts", ciphertext3);
  RequireAppend(base::as_byte_span(cleartext));
  rendition->CheckState(base::Seconds(0), 0.0,
                        BindCheckState(base::Seconds(0)));
  task_environment_.RunUntilIdle();
}

}  // namespace media