| // 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 <memory> |
| #include <string> |
| #include <vector> |
| |
| #include "base/memory/scoped_refptr.h" |
| #include "base/run_loop.h" |
| #include "base/test/gmock_callback_support.h" |
| #include "base/test/simple_test_tick_clock.h" |
| #include "base/test/task_environment.h" |
| #include "media/base/cross_origin_data_source.h" |
| #include "media/base/mock_media_log.h" |
| #include "media/filters/hls_data_source_provider_impl.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace media { |
| |
| using base::test::RunOnceCallback; |
| using testing::_; |
| using testing::AtLeast; |
| using testing::ByMove; |
| using testing::DoAll; |
| using testing::Eq; |
| using testing::Invoke; |
| using testing::NiceMock; |
| using testing::NotNull; |
| using testing::Ref; |
| using testing::Return; |
| using testing::SaveArg; |
| using testing::SetArgPointee; |
| using testing::StrictMock; |
| |
| namespace { |
| |
| class MockDataSource : public CrossOriginDataSource { |
| public: |
| // Mocked methods from CrossOriginDataSource |
| MOCK_METHOD(bool, IsCorsCrossOrigin, (), (const, override)); |
| MOCK_METHOD(bool, HasAccessControl, (), (const, override)); |
| MOCK_METHOD(const std::string&, GetMimeType, (), (const, override)); |
| MOCK_METHOD(void, |
| Initialize, |
| (base::OnceCallback<void(bool)> init_cb), |
| (override)); |
| |
| // Mocked methods from DataSource |
| MOCK_METHOD( |
| void, |
| Read, |
| (int64_t position, int size, uint8_t* data, DataSource::ReadCB read_cb), |
| (override)); |
| MOCK_METHOD(void, Stop, (), (override)); |
| MOCK_METHOD(void, Abort, (), (override)); |
| MOCK_METHOD(bool, GetSize, (int64_t * size_out), (override)); |
| MOCK_METHOD(bool, IsStreaming, (), (override)); |
| MOCK_METHOD(void, SetBitrate, (int bitrate), (override)); |
| MOCK_METHOD(bool, PassedTimingAllowOriginCheck, (), (override)); |
| MOCK_METHOD(bool, WouldTaintOrigin, (), (override)); |
| MOCK_METHOD(bool, AssumeFullyBuffered, (), (const, override)); |
| MOCK_METHOD(int64_t, GetMemoryUsage, (), (override)); |
| MOCK_METHOD(void, |
| SetPreload, |
| (DataSource::Preload preload), |
| (override)); |
| MOCK_METHOD(GURL, GetUrlAfterRedirects, (), (const, override)); |
| MOCK_METHOD(void, |
| OnBufferingHaveEnough, |
| (bool must_cancel_netops), |
| (override)); |
| MOCK_METHOD(void, |
| OnMediaPlaybackRateChanged, |
| (double playback_rate), |
| (override)); |
| MOCK_METHOD(void, OnMediaIsPlaying, (), (override)); |
| CrossOriginDataSource* GetAsCrossOriginDataSource() override { return this; } |
| }; |
| |
| class MockDataSourceFactory |
| : public HlsDataSourceProviderImpl::DataSourceFactory { |
| public: |
| using MockDataSource = NiceMock<MockDataSource>; |
| |
| ~MockDataSourceFactory() override = default; |
| MockDataSourceFactory() = default; |
| void CreateDataSource(GURL uri, DataSourceCb cb) override { |
| if (!next_mock_) { |
| PregenerateNextMock(); |
| EXPECT_CALL(*next_mock_, Initialize).WillOnce(RunOnceCallback<0>(true)); |
| for (const auto& e : read_expectations_) { |
| EXPECT_CALL(*next_mock_, Read(std::get<0>(e), std::get<1>(e), _, _)) |
| .WillOnce(RunOnceCallback<3>(std::get<2>(e))); |
| } |
| read_expectations_.clear(); |
| EXPECT_CALL(*next_mock_, Stop()); |
| } |
| std::move(cb).Run(std::move(next_mock_)); |
| } |
| |
| void AddReadExpectation(size_t from, size_t to, int response) { |
| read_expectations_.emplace_back(from, to, response); |
| } |
| |
| MockDataSource* PregenerateNextMock() { |
| next_mock_ = std::make_unique<MockDataSource>(); |
| return next_mock_.get(); |
| } |
| |
| private: |
| std::unique_ptr<MockDataSource> next_mock_; |
| std::vector<std::tuple<size_t, size_t, int>> read_expectations_; |
| }; |
| |
| } // namespace |
| |
| class HlsDataSourceProviderImplUnittest : public testing::Test { |
| public: |
| ~HlsDataSourceProviderImplUnittest() override = default; |
| HlsDataSourceProviderImplUnittest() { RecreateImpl(); } |
| |
| void RecreateImpl() { |
| auto factory = std::make_unique<MockDataSourceFactory>(); |
| factory_ = factory.get(); |
| impl_ = std::make_unique<HlsDataSourceProviderImpl>(std::move(factory)); |
| } |
| |
| protected: |
| base::test::TaskEnvironment task_environment_; |
| std::unique_ptr<HlsDataSourceProviderImpl> impl_; |
| |
| raw_ptr<MockDataSourceFactory> factory_; |
| }; |
| |
| TEST_F(HlsDataSourceProviderImplUnittest, TestReadFromUrlOnce) { |
| // The entire read is satisfied, so there is more to read. |
| factory_->AddReadExpectation(0, 16384, 16384); |
| impl_->ReadFromUrl( |
| {GURL("example.com"), std::nullopt}, |
| base::BindOnce([](HlsDataSourceProvider::ReadResult result) { |
| ASSERT_TRUE(result.has_value()); |
| auto stream = std::move(result).value(); |
| ASSERT_EQ(stream->read_position(), 16384lu); |
| ASSERT_EQ(stream->buffer_size(), 16384lu); |
| ASSERT_EQ(stream->max_read_position(), std::nullopt); |
| ASSERT_TRUE(stream->CanReadMore()); |
| })); |
| task_environment_.RunUntilIdle(); |
| |
| // Only got 400 bytes of requested, so the stream is _probably_ ended, but |
| // we'd have to read again (and get a 0) to be sure. |
| factory_->AddReadExpectation(0, 16384, 400); |
| impl_->ReadFromUrl( |
| {GURL("example.com"), std::nullopt}, |
| base::BindOnce([](HlsDataSourceProvider::ReadResult result) { |
| ASSERT_TRUE(result.has_value()); |
| auto stream = std::move(result).value(); |
| ASSERT_TRUE(stream->CanReadMore()); |
| ASSERT_EQ(stream->read_position(), 400lu); |
| ASSERT_EQ(stream->buffer_size(), 16384lu); |
| ASSERT_EQ(stream->max_read_position(), std::nullopt); |
| })); |
| task_environment_.RunUntilIdle(); |
| |
| // The data source should only be limited to 4242 total bytes and should start |
| // at an offset of 99. The read should be from 99, size of 4242. |
| factory_->AddReadExpectation(99, 4242, 4242); |
| impl_->ReadFromUrl( |
| {GURL("example.com"), hls::types::ByteRange::Validate(4242, 99)}, |
| base::BindOnce([](HlsDataSourceProvider::ReadResult result) { |
| ASSERT_TRUE(result.has_value()); |
| auto stream = std::move(result).value(); |
| ASSERT_EQ(stream->read_position(), 4341lu); |
| ASSERT_EQ(stream->buffer_size(), 4242lu); |
| ASSERT_EQ(stream->max_read_position().value_or(0), 4341lu); |
| ASSERT_FALSE(stream->CanReadMore()); |
| })); |
| task_environment_.RunUntilIdle(); |
| } |
| |
| TEST_F(HlsDataSourceProviderImplUnittest, TestReadFromUrlThenReadAgain) { |
| factory_->AddReadExpectation(0, 16384, 16384); |
| factory_->AddReadExpectation(16384, 16384, 16384); |
| factory_->AddReadExpectation(32768, 16384, 3); |
| factory_->AddReadExpectation(32771, 16384, 0); |
| impl_->ReadFromUrl( |
| {GURL("example.com"), std::nullopt}, |
| base::BindOnce( |
| [](HlsDataSourceProviderImpl* impl_ptr, |
| HlsDataSourceProvider::ReadResult result) { |
| ASSERT_TRUE(result.has_value()); |
| auto stream = std::move(result).value(); |
| ASSERT_EQ(stream->read_position(), 16384lu); |
| ASSERT_EQ(stream->buffer_size(), 16384lu); |
| ASSERT_TRUE(stream->CanReadMore()); |
| |
| impl_ptr->ReadFromExistingStream( |
| std::move(stream), |
| base::BindOnce( |
| [](HlsDataSourceProviderImpl* impl_ptr, |
| HlsDataSourceProvider::ReadResult result) { |
| ASSERT_TRUE(result.has_value()); |
| auto stream = std::move(result).value(); |
| ASSERT_EQ(stream->read_position(), 32768lu); |
| ASSERT_EQ(stream->buffer_size(), 32768lu); |
| ASSERT_TRUE(stream->CanReadMore()); |
| |
| impl_ptr->ReadFromExistingStream( |
| std::move(stream), |
| base::BindOnce( |
| [](HlsDataSourceProviderImpl* impl_ptr, |
| HlsDataSourceProvider::ReadResult result) { |
| ASSERT_TRUE(result.has_value()); |
| auto stream = std::move(result).value(); |
| ASSERT_EQ(stream->read_position(), 32771lu); |
| ASSERT_EQ(stream->buffer_size(), 49152lu); |
| ASSERT_TRUE(stream->CanReadMore()); |
| |
| impl_ptr->ReadFromExistingStream( |
| std::move(stream), |
| base::BindOnce( |
| [](HlsDataSourceProvider::ReadResult |
| result) { |
| ASSERT_TRUE(result.has_value()); |
| auto stream = |
| std::move(result).value(); |
| ASSERT_EQ(stream->read_position(), |
| 32771lu); |
| ASSERT_EQ(stream->buffer_size(), |
| 32771lu); |
| ASSERT_FALSE(stream->CanReadMore()); |
| })); |
| }, |
| impl_ptr)); |
| }, |
| impl_ptr)); |
| }, |
| impl_.get())); |
| |
| task_environment_.RunUntilIdle(); |
| } |
| |
| TEST_F(HlsDataSourceProviderImplUnittest, TestAbortMidDownload) { |
| // Pregenerating the mock requires setting all our own expectations. |
| auto* mock_data_source = factory_->PregenerateNextMock(); |
| EXPECT_CALL(*mock_data_source, Initialize).WillOnce(RunOnceCallback<0>(true)); |
| EXPECT_CALL(*mock_data_source, Abort()).Times(0); |
| EXPECT_CALL(*mock_data_source, Stop()).Times(0); |
| |
| DataSource::ReadCB read_cb; |
| EXPECT_CALL(*mock_data_source, Read(0, _, _, _)) |
| .WillOnce( |
| [&read_cb](int64_t, int, uint8_t*, DataSource::ReadCB cb) { |
| read_cb = std::move(cb); |
| }); |
| |
| // The Read CB is captured, and so will not execute right away. |
| bool has_been_read = false; |
| impl_->ReadFromUrl( |
| {GURL("example.com"), std::nullopt}, |
| base::BindOnce( |
| [](bool* read_canary, HlsDataSourceProvider::ReadResult result) { |
| *read_canary = true; |
| }, |
| &has_been_read)); |
| |
| // cycle everything and check that we are blocking the read. |
| task_environment_.RunUntilIdle(); |
| ASSERT_FALSE(has_been_read); |
| ASSERT_TRUE(!!read_cb); |
| |
| // Deleting the HlsDataSourceproviderImpl will stop all existing reads. |
| EXPECT_CALL(*mock_data_source, Stop()); |
| RecreateImpl(); |
| task_environment_.RunUntilIdle(); |
| |
| // Run with aborted signal. |
| std::move(read_cb).Run(-2); |
| |
| task_environment_.RunUntilIdle(); |
| ASSERT_TRUE(has_been_read); |
| } |
| |
| TEST_F(HlsDataSourceProviderImplUnittest, AbortMidInit) { |
| auto* mock_data_source = factory_->PregenerateNextMock(); |
| |
| // Don't run init cb! |
| EXPECT_CALL(*mock_data_source, Initialize); |
| EXPECT_CALL(*mock_data_source, Stop()); |
| |
| bool has_been_read = false; |
| impl_->ReadFromUrl( |
| {GURL("example.com"), std::nullopt}, |
| base::BindOnce( |
| [](bool* read_canary, HlsDataSourceProvider::ReadResult result) { |
| *read_canary = true; |
| }, |
| &has_been_read)); |
| |
| // Despite the init never returning, it is stored in the `data_source_map_` |
| // and all entries there get stopped on teardown. |
| task_environment_.RunUntilIdle(); |
| |
| // Should be false, because the stream init function won't post it's callback. |
| ASSERT_FALSE(has_been_read); |
| } |
| |
| TEST_F(HlsDataSourceProviderImplUnittest, ReadMultipleSegments) { |
| HlsDataSourceProvider::SegmentQueue segments; |
| segments.emplace(GURL("example.com"), std::nullopt); |
| segments.emplace(GURL("foo.com"), std::nullopt); |
| |
| // Request 16k, but only 4k is read. Another read then happens and the 0 byte |
| // EOS read happens. |
| factory_->AddReadExpectation(0, 16384, 4096); |
| factory_->AddReadExpectation(4096, 16384, 0); |
| |
| std::unique_ptr<HlsDataSourceStream> read_result; |
| impl_->ReadFromCombinedUrlQueue( |
| std::move(segments), base::BindOnce( |
| [](std::unique_ptr<HlsDataSourceStream>* extract, |
| HlsDataSourceProvider::ReadResult result) { |
| *extract = std::move(result).value(); |
| }, |
| &read_result)); |
| task_environment_.RunUntilIdle(); |
| ASSERT_NE(read_result, nullptr); |
| ASSERT_TRUE(read_result->CanReadMore()); |
| ASSERT_FALSE(read_result->RequiresNextDataSource()); |
| |
| impl_->ReadFromExistingStream( |
| std::move(read_result), |
| base::BindOnce( |
| [](std::unique_ptr<HlsDataSourceStream>* extract, |
| HlsDataSourceProvider::ReadResult result) { |
| *extract = std::move(result).value(); |
| }, |
| &read_result)); |
| task_environment_.RunUntilIdle(); |
| ASSERT_NE(read_result, nullptr); |
| ASSERT_TRUE(read_result->CanReadMore()); |
| ASSERT_TRUE(read_result->RequiresNextDataSource()); |
| |
| factory_->AddReadExpectation(0, 16384, 4096); |
| factory_->AddReadExpectation(4096, 16384, 0); |
| impl_->ReadFromExistingStream( |
| std::move(read_result), |
| base::BindOnce( |
| [](std::unique_ptr<HlsDataSourceStream>* extract, |
| HlsDataSourceProvider::ReadResult result) { |
| *extract = std::move(result).value(); |
| }, |
| &read_result)); |
| |
| task_environment_.RunUntilIdle(); |
| ASSERT_NE(read_result, nullptr); |
| ASSERT_TRUE(read_result->CanReadMore()); |
| ASSERT_FALSE(read_result->RequiresNextDataSource()); |
| |
| impl_->ReadFromExistingStream( |
| std::move(read_result), |
| base::BindOnce( |
| [](std::unique_ptr<HlsDataSourceStream>* extract, |
| HlsDataSourceProvider::ReadResult result) { |
| *extract = std::move(result).value(); |
| }, |
| &read_result)); |
| |
| task_environment_.RunUntilIdle(); |
| ASSERT_NE(read_result, nullptr); |
| ASSERT_FALSE(read_result->CanReadMore()); |
| ASSERT_FALSE(read_result->RequiresNextDataSource()); |
| |
| read_result = nullptr; |
| task_environment_.RunUntilIdle(); |
| } |
| |
| TEST_F(HlsDataSourceProviderImplUnittest, ReadMultipleRangedSegments) { |
| HlsDataSourceProvider::SegmentQueue segments; |
| // Read 10 bytes from offset 0. |
| segments.emplace(GURL("example.com"), hls::types::ByteRange::Validate(10, 0)); |
| |
| // Read 100 bytes from offset 100 |
| segments.emplace(GURL("foo.com"), hls::types::ByteRange::Validate(100, 100)); |
| |
| // Request 10 bytes from the 0 offset. this is fairly common for EXT-X-MAP. |
| factory_->AddReadExpectation(0, 10, 10); |
| |
| std::unique_ptr<HlsDataSourceStream> read_result; |
| impl_->ReadFromCombinedUrlQueue( |
| std::move(segments), base::BindOnce( |
| [](std::unique_ptr<HlsDataSourceStream>* extract, |
| HlsDataSourceProvider::ReadResult result) { |
| *extract = std::move(result).value(); |
| }, |
| &read_result)); |
| |
| task_environment_.RunUntilIdle(); |
| ASSERT_NE(read_result, nullptr); |
| ASSERT_TRUE(read_result->CanReadMore()); |
| ASSERT_TRUE(read_result->RequiresNextDataSource()); |
| |
| // The second segment will request another 100 bytes, and again, because it is |
| // a range request, more should be returned. |
| factory_->AddReadExpectation(100, 100, 100); |
| impl_->ReadFromExistingStream( |
| std::move(read_result), |
| base::BindOnce( |
| [](std::unique_ptr<HlsDataSourceStream>* extract, |
| HlsDataSourceProvider::ReadResult result) { |
| *extract = std::move(result).value(); |
| }, |
| &read_result)); |
| |
| task_environment_.RunUntilIdle(); |
| ASSERT_NE(read_result, nullptr); |
| ASSERT_FALSE(read_result->CanReadMore()); |
| ASSERT_FALSE(read_result->RequiresNextDataSource()); |
| |
| read_result = nullptr; |
| task_environment_.RunUntilIdle(); |
| } |
| |
| } // namespace blink |