| // Copyright 2020 The Cobalt Authors. All Rights Reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| #include "starboard/common/time.h" |
| #include "starboard/nplb/drm_helpers.h" |
| #include "starboard/nplb/player_creation_param_helpers.h" |
| #include "starboard/nplb/player_test_fixture.h" |
| #include "starboard/nplb/player_test_util.h" |
| #include "starboard/nplb/posix_compliance/posix_thread_helpers.h" |
| #include "starboard/string.h" |
| #include "starboard/testing/fake_graphics_context_provider.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace starboard { |
| namespace nplb { |
| namespace { |
| |
| using ::testing::ValuesIn; |
| |
| typedef SbPlayerTestFixture::GroupedSamples GroupedSamples; |
| typedef testing::FakeGraphicsContextProvider FakeGraphicsContextProvider; |
| |
| class SbPlayerWriteSampleTest |
| : public ::testing::TestWithParam<SbPlayerTestConfig> { |
| protected: |
| FakeGraphicsContextProvider fake_graphics_context_provider_; |
| }; |
| |
| TEST_P(SbPlayerWriteSampleTest, SeekAndDestroy) { |
| SbPlayerTestFixture player_fixture(GetParam(), |
| &fake_graphics_context_provider_); |
| if (HasFatalFailure()) { |
| return; |
| } |
| ASSERT_NO_FATAL_FAILURE(player_fixture.Seek(1'000'000)); |
| } |
| |
| TEST_P(SbPlayerWriteSampleTest, NoInput) { |
| SbPlayerTestFixture player_fixture(GetParam(), |
| &fake_graphics_context_provider_); |
| if (HasFatalFailure()) { |
| return; |
| } |
| |
| GroupedSamples samples; |
| if (player_fixture.HasAudio()) { |
| samples.AddAudioEOS(); |
| } |
| if (player_fixture.HasVideo()) { |
| samples.AddVideoEOS(); |
| } |
| ASSERT_NO_FATAL_FAILURE(player_fixture.Write(samples)); |
| ASSERT_NO_FATAL_FAILURE(player_fixture.WaitForPlayerEndOfStream()); |
| } |
| |
| TEST_P(SbPlayerWriteSampleTest, WriteSingleBatch) { |
| SbPlayerTestFixture player_fixture(GetParam(), |
| &fake_graphics_context_provider_); |
| if (HasFatalFailure()) { |
| return; |
| } |
| |
| GroupedSamples samples; |
| if (player_fixture.HasAudio()) { |
| int samples_to_write = SbPlayerGetMaximumNumberOfSamplesPerWrite( |
| player_fixture.GetPlayer(), kSbMediaTypeAudio); |
| samples.AddAudioSamples(0, samples_to_write); |
| samples.AddAudioEOS(); |
| } |
| if (player_fixture.HasVideo()) { |
| int samples_to_write = SbPlayerGetMaximumNumberOfSamplesPerWrite( |
| player_fixture.GetPlayer(), kSbMediaTypeVideo); |
| samples.AddVideoSamples(0, samples_to_write); |
| samples.AddVideoEOS(); |
| } |
| |
| ASSERT_NO_FATAL_FAILURE(player_fixture.Write(samples)); |
| ASSERT_NO_FATAL_FAILURE(player_fixture.WaitForPlayerEndOfStream()); |
| } |
| |
| TEST_P(SbPlayerWriteSampleTest, WriteMultipleBatches) { |
| SbPlayerTestFixture player_fixture(GetParam(), |
| &fake_graphics_context_provider_); |
| if (HasFatalFailure()) { |
| return; |
| } |
| |
| int samples_to_write = 0; |
| // Try to write multiple batches for both audio and video. |
| if (player_fixture.HasAudio()) { |
| samples_to_write = SbPlayerGetMaximumNumberOfSamplesPerWrite( |
| player_fixture.GetPlayer(), kSbMediaTypeAudio) + |
| 1; |
| } |
| if (player_fixture.HasVideo()) { |
| samples_to_write = std::max( |
| samples_to_write, SbPlayerGetMaximumNumberOfSamplesPerWrite( |
| player_fixture.GetPlayer(), kSbMediaTypeVideo) + |
| 1); |
| } |
| // TODO(b/283533109): We'd better to align the written audio and video samples |
| // to a same timestamp. Currently, we simply cap the batch size to 8 samples. |
| samples_to_write = std::min(samples_to_write, 8); |
| |
| GroupedSamples samples; |
| if (player_fixture.HasAudio()) { |
| samples.AddAudioSamples(0, samples_to_write); |
| samples.AddAudioEOS(); |
| } |
| if (player_fixture.HasVideo()) { |
| samples.AddVideoSamples(0, samples_to_write); |
| samples.AddVideoEOS(); |
| } |
| |
| ASSERT_NO_FATAL_FAILURE(player_fixture.Write(samples)); |
| ASSERT_NO_FATAL_FAILURE(player_fixture.WaitForPlayerEndOfStream()); |
| } |
| |
| TEST_P(SbPlayerWriteSampleTest, LimitedAudioInput) { |
| SbPlayerTestFixture player_fixture(GetParam(), |
| &fake_graphics_context_provider_); |
| if (HasFatalFailure()) { |
| return; |
| } |
| |
| // TODO: we simply set audio write duration to 0.5 second. Ideally, we should |
| // set the audio write duration to 10 seconds if audio connectors are remote. |
| player_fixture.SetAudioWriteDuration(500'000); |
| |
| GroupedSamples samples; |
| if (player_fixture.HasAudio()) { |
| samples.AddAudioSamples( |
| 0, player_fixture.ConvertDurationToAudioBufferCount(1'000'000)); |
| samples.AddAudioEOS(); |
| } |
| if (player_fixture.HasVideo()) { |
| samples.AddVideoSamples( |
| 0, player_fixture.ConvertDurationToVideoBufferCount(1'000'000)); |
| samples.AddVideoEOS(); |
| } |
| |
| ASSERT_NO_FATAL_FAILURE(player_fixture.Write(samples)); |
| ASSERT_NO_FATAL_FAILURE(player_fixture.WaitForPlayerEndOfStream()); |
| } |
| |
| TEST_P(SbPlayerWriteSampleTest, PartialAudio) { |
| if (!IsPartialAudioSupported()) { |
| // TODO: Use GTEST_SKIP when we have a newer version of gtest. |
| SB_LOG(INFO) |
| << "The platform doesn't support partial audio. Skip the tests."; |
| return; |
| } |
| if (IsAudioPassthroughUsed(GetParam())) { |
| SB_LOG(INFO) << "The audio passthrough doesn't support partial audio. Skip " |
| "the tests."; |
| return; |
| } |
| |
| SbPlayerTestFixture player_fixture(GetParam(), |
| &fake_graphics_context_provider_); |
| if (HasFatalFailure()) { |
| return; |
| } |
| if (!player_fixture.HasAudio()) { |
| // TODO: Use GTEST_SKIP when we have a newer version of gtest. |
| SB_LOG(INFO) << "Skip PartialAudio test for audioless content."; |
| return; |
| } |
| |
| const int64_t kDurationToPlay = 1'000'000; // 1 second |
| const float kSegmentSize = 0.3f; |
| |
| GroupedSamples samples; |
| if (player_fixture.HasVideo()) { |
| samples.AddVideoSamples( |
| 0, player_fixture.ConvertDurationToVideoBufferCount(kDurationToPlay)); |
| samples.AddVideoEOS(); |
| } |
| |
| int total_buffers_to_write = |
| player_fixture.ConvertDurationToAudioBufferCount(kDurationToPlay); |
| for (int i = 0; i < total_buffers_to_write; i++) { |
| int64_t current_timestamp = player_fixture.GetAudioSampleTimestamp(i); |
| int64_t next_timestamp = player_fixture.GetAudioSampleTimestamp(i + 1); |
| int64_t buffer_duration = next_timestamp - current_timestamp; |
| int64_t segment_duration = buffer_duration * kSegmentSize; |
| int64_t written_duration = 0; |
| while (written_duration < buffer_duration) { |
| samples.AddAudioSamples( |
| i, 1, written_duration, written_duration, |
| std::max<int64_t>( |
| 0, buffer_duration - written_duration - segment_duration)); |
| written_duration += segment_duration; |
| } |
| } |
| samples.AddAudioEOS(); |
| |
| ASSERT_NO_FATAL_FAILURE(player_fixture.Write(samples)); |
| ASSERT_NO_FATAL_FAILURE(player_fixture.WaitForPlayerPresenting()); |
| |
| int64_t start_system_time = CurrentMonotonicTime(); |
| int64_t start_media_time = player_fixture.GetCurrentMediaTime(); |
| |
| ASSERT_NO_FATAL_FAILURE(player_fixture.WaitForPlayerEndOfStream()); |
| |
| int64_t end_system_time = CurrentMonotonicTime(); |
| int64_t end_media_time = player_fixture.GetCurrentMediaTime(); |
| |
| const int64_t kDurationDifferenceAllowance = 500'000; // 500ms; |
| EXPECT_NEAR(end_media_time, kDurationToPlay, kDurationDifferenceAllowance); |
| EXPECT_NEAR(end_system_time - start_system_time + start_media_time, |
| kDurationToPlay, kDurationDifferenceAllowance); |
| |
| SB_DLOG(INFO) << "The expected media time should be " << kDurationToPlay |
| << ", the actual media time is " << end_media_time |
| << ", with difference " |
| << std::abs(end_media_time - kDurationToPlay) << "."; |
| SB_DLOG(INFO) << "The expected total playing time should be " |
| << kDurationToPlay << ", the actual playing time is " |
| << end_system_time - start_system_time + start_media_time |
| << ", with difference " |
| << std::abs(end_system_time - start_system_time + |
| start_media_time - kDurationToPlay) |
| << "."; |
| } |
| |
| TEST_P(SbPlayerWriteSampleTest, DiscardAllAudio) { |
| if (!IsPartialAudioSupported()) { |
| // TODO: Use GTEST_SKIP when we have a newer version of gtest. |
| SB_LOG(INFO) |
| << "The platform doesn't support partial audio. Skip the tests."; |
| return; |
| } |
| if (IsAudioPassthroughUsed(GetParam())) { |
| SB_LOG(INFO) << "The audio passthrough doesn't support partial audio. Skip " |
| "the tests."; |
| return; |
| } |
| |
| SbPlayerTestFixture player_fixture(GetParam(), |
| &fake_graphics_context_provider_); |
| if (HasFatalFailure()) { |
| return; |
| } |
| if (!player_fixture.HasAudio()) { |
| // TODO: Use GTEST_SKIP when we have a newer version of gtest. |
| SB_LOG(INFO) << "Skip PartialAudio test for audioless content."; |
| return; |
| } |
| |
| const int64_t kDurationToPlay = 1'000'000; // 1 second |
| const int64_t kDurationPerWrite = 100'000; // 100ms |
| const int64_t kNumberOfBuffersToDiscard = 20; |
| |
| GroupedSamples samples; |
| if (player_fixture.HasVideo()) { |
| samples.AddVideoSamples( |
| 0, player_fixture.ConvertDurationToVideoBufferCount(kDurationToPlay)); |
| samples.AddVideoEOS(); |
| } |
| |
| int written_buffer_index = 0; |
| int64_t current_time_offset = 0; |
| int num_of_buffers_per_write = |
| player_fixture.ConvertDurationToAudioBufferCount(kDurationPerWrite); |
| int count = 0; |
| while (current_time_offset < kDurationToPlay) { |
| const int64_t kDurationToDiscard = |
| count % 2 == 0 ? 1'000'000LL : kSbInt64Max; |
| count++; |
| // Discard from front. |
| for (int i = 0; i < kNumberOfBuffersToDiscard; i++) { |
| samples.AddAudioSamples(written_buffer_index, 1, current_time_offset, |
| kDurationToDiscard, 0); |
| } |
| |
| samples.AddAudioSamples(written_buffer_index, num_of_buffers_per_write); |
| written_buffer_index += num_of_buffers_per_write; |
| current_time_offset += kDurationPerWrite; |
| |
| // Discard from back. |
| for (int i = 0; i < kNumberOfBuffersToDiscard; i++) { |
| samples.AddAudioSamples(written_buffer_index, 1, current_time_offset, 0, |
| kDurationToDiscard); |
| } |
| } |
| samples.AddAudioEOS(); |
| |
| ASSERT_NO_FATAL_FAILURE(player_fixture.Write(samples)); |
| ASSERT_NO_FATAL_FAILURE(player_fixture.WaitForPlayerPresenting()); |
| |
| int64_t start_system_time = CurrentMonotonicTime(); |
| int64_t start_media_time = player_fixture.GetCurrentMediaTime(); |
| |
| ASSERT_NO_FATAL_FAILURE(player_fixture.WaitForPlayerEndOfStream()); |
| |
| int64_t end_system_time = CurrentMonotonicTime(); |
| int64_t end_media_time = player_fixture.GetCurrentMediaTime(); |
| |
| const int64_t kDurationDifferenceAllowance = 500'000; // 500ms |
| int64_t total_written_duration = |
| player_fixture.GetAudioSampleTimestamp(written_buffer_index); |
| EXPECT_NEAR(end_media_time, total_written_duration, |
| kDurationDifferenceAllowance); |
| EXPECT_NEAR(end_system_time - start_system_time + start_media_time, |
| total_written_duration, kDurationDifferenceAllowance); |
| |
| SB_DLOG(INFO) << "The expected media time should be " |
| << total_written_duration << ", the actual media time is " |
| << end_media_time << ", with difference " |
| << std::abs(end_media_time - total_written_duration) << "."; |
| SB_DLOG(INFO) << "The expected total playing time should be " |
| << total_written_duration << ", the actual playing time is " |
| << end_system_time - start_system_time + start_media_time |
| << ", with difference " |
| << std::abs(end_system_time - start_system_time + |
| start_media_time - total_written_duration) |
| << "."; |
| } |
| |
| class SecondaryPlayerTestThread : public posix::AbstractTestThread { |
| public: |
| SecondaryPlayerTestThread( |
| const SbPlayerTestConfig& config, |
| FakeGraphicsContextProvider* fake_graphics_context_provider) |
| : config_(config), |
| fake_graphics_context_provider_(fake_graphics_context_provider) {} |
| |
| void Run() override { |
| SbPlayerTestFixture player_fixture(config_, |
| fake_graphics_context_provider_); |
| if (::testing::Test::HasFatalFailure()) { |
| return; |
| } |
| |
| const int64_t kDurationToPlay = 200'000; // 200ms |
| |
| GroupedSamples samples; |
| if (player_fixture.HasAudio()) { |
| samples.AddAudioSamples( |
| 0, player_fixture.ConvertDurationToAudioBufferCount(kDurationToPlay)); |
| samples.AddAudioEOS(); |
| } |
| if (player_fixture.HasVideo()) { |
| samples.AddVideoSamples( |
| 0, player_fixture.ConvertDurationToVideoBufferCount(kDurationToPlay)); |
| samples.AddVideoEOS(); |
| } |
| |
| ASSERT_NO_FATAL_FAILURE(player_fixture.Write(samples)); |
| ASSERT_NO_FATAL_FAILURE(player_fixture.WaitForPlayerEndOfStream()); |
| } |
| |
| private: |
| const SbPlayerTestConfig config_; |
| FakeGraphicsContextProvider* fake_graphics_context_provider_; |
| }; |
| |
| TEST_P(SbPlayerWriteSampleTest, SecondaryPlayerTest) { |
| // The secondary player should at least support h264 at 480p, 30fps and with |
| // a drm system. |
| const char* kMaxVideoCapabilities = "width=640; height=480; framerate=30;"; |
| const char* kSecondaryTestFile = "beneath_the_canopy_135_avc.dmp"; |
| const char* key_system = GetParam().key_system; |
| |
| SbPlayerTestConfig secondary_player_config(nullptr, kSecondaryTestFile, |
| kSbPlayerOutputModeInvalid, |
| key_system, kMaxVideoCapabilities); |
| PlayerCreationParam creation_param = |
| CreatePlayerCreationParam(secondary_player_config); |
| secondary_player_config.output_mode = GetPreferredOutputMode(creation_param); |
| |
| ASSERT_NE(secondary_player_config.output_mode, kSbPlayerOutputModeInvalid); |
| |
| SecondaryPlayerTestThread primary_player_thread( |
| GetParam(), &fake_graphics_context_provider_); |
| SecondaryPlayerTestThread secondary_player_thread( |
| secondary_player_config, &fake_graphics_context_provider_); |
| |
| primary_player_thread.Start(); |
| secondary_player_thread.Start(); |
| |
| primary_player_thread.Join(); |
| secondary_player_thread.Join(); |
| } |
| |
| std::vector<SbPlayerTestConfig> GetSupportedTestConfigs() { |
| static std::vector<SbPlayerTestConfig> supported_configs; |
| if (supported_configs.size() > 0) { |
| return supported_configs; |
| } |
| |
| const std::vector<const char*>& key_systems = GetKeySystems(); |
| for (auto key_system : key_systems) { |
| std::vector<SbPlayerTestConfig> configs = |
| GetSupportedSbPlayerTestConfigs(key_system); |
| supported_configs.insert(supported_configs.end(), configs.begin(), |
| configs.end()); |
| } |
| return supported_configs; |
| } |
| |
| INSTANTIATE_TEST_CASE_P(SbPlayerWriteSampleTests, |
| SbPlayerWriteSampleTest, |
| ValuesIn(GetSupportedTestConfigs()), |
| GetSbPlayerTestConfigName); |
| |
| } // namespace |
| } // namespace nplb |
| } // namespace starboard |