| // Copyright 2018 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/shared/starboard/player/filter/video_decoder_internal.h" |
| |
| #include <algorithm> |
| #include <deque> |
| #include <functional> |
| #include <map> |
| #include <set> |
| |
| #include "starboard/common/condition_variable.h" |
| #include "starboard/common/mutex.h" |
| #include "starboard/common/scoped_ptr.h" |
| #include "starboard/common/string.h" |
| #include "starboard/drm.h" |
| #include "starboard/media.h" |
| #include "starboard/memory.h" |
| #include "starboard/shared/starboard/media/media_support_internal.h" |
| #include "starboard/shared/starboard/media/media_util.h" |
| #include "starboard/shared/starboard/player/filter/stub_player_components_impl.h" |
| #include "starboard/shared/starboard/player/job_queue.h" |
| #include "starboard/shared/starboard/player/video_dmp_reader.h" |
| #include "starboard/testing/fake_graphics_context_provider.h" |
| #include "starboard/thread.h" |
| #include "starboard/time.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| #if SB_HAS(PLAYER_FILTER_TESTS) |
| // This has to be defined in the global namespace as its instance will be used |
| // as SbPlayer. |
| struct SbPlayerPrivate {}; |
| |
| namespace starboard { |
| namespace shared { |
| namespace starboard { |
| namespace player { |
| namespace filter { |
| namespace testing { |
| namespace { |
| |
| using ::starboard::testing::FakeGraphicsContextProvider; |
| using ::std::placeholders::_1; |
| using ::std::placeholders::_2; |
| using ::testing::AssertionFailure; |
| using ::testing::AssertionResult; |
| using ::testing::AssertionSuccess; |
| using ::testing::Bool; |
| using ::testing::Combine; |
| using ::testing::ValuesIn; |
| using video_dmp::VideoDmpReader; |
| |
| struct TestParam { |
| SbPlayerOutputMode output_mode; |
| const char* filename; |
| }; |
| |
| const SbTimeMonotonic kDefaultWaitForNextEventTimeOut = 5 * kSbTimeSecond; |
| |
| std::string GetTestInputDirectory() { |
| const size_t kPathSize = SB_FILE_MAX_PATH + 1; |
| |
| char content_path[kPathSize]; |
| EXPECT_TRUE( |
| SbSystemGetPath(kSbSystemPathContentDirectory, content_path, kPathSize)); |
| std::string directory_path = |
| std::string(content_path) + SB_FILE_SEP_CHAR + "test" + SB_FILE_SEP_CHAR + |
| "starboard" + SB_FILE_SEP_CHAR + "shared" + SB_FILE_SEP_CHAR + |
| "starboard" + SB_FILE_SEP_CHAR + "player" + SB_FILE_SEP_CHAR + "testdata"; |
| |
| SB_CHECK(SbDirectoryCanOpen(directory_path.c_str())) << directory_path; |
| return directory_path; |
| } |
| |
| void DeallocateSampleFunc(SbPlayer player, |
| void* context, |
| const void* sample_buffer) { |
| SB_UNREFERENCED_PARAMETER(player); |
| SB_UNREFERENCED_PARAMETER(context); |
| SB_UNREFERENCED_PARAMETER(sample_buffer); |
| } |
| |
| std::string ResolveTestFileName(const char* filename) { |
| return GetTestInputDirectory() + SB_FILE_SEP_CHAR + filename; |
| } |
| |
| AssertionResult AlmostEqualTime(SbTime time1, SbTime time2) { |
| const SbTime kEpsilon = kSbTimeSecond / 1000; |
| SbTime diff = time1 - time2; |
| if (-kEpsilon <= diff && diff <= kEpsilon) { |
| return AssertionSuccess(); |
| } |
| return AssertionFailure() |
| << "time " << time1 << " doesn't match with time " << time2; |
| } |
| |
| class VideoDecoderTest |
| : public ::testing::TestWithParam<std::tuple<TestParam, bool>> { |
| public: |
| VideoDecoderTest() |
| : test_filename_(std::get<0>(GetParam()).filename), |
| output_mode_(std::get<0>(GetParam()).output_mode), |
| using_stub_decoder_(std::get<1>(GetParam())), |
| dmp_reader_(ResolveTestFileName(test_filename_).c_str()) { |
| SB_LOG(INFO) << "Testing " << test_filename_ << ", output mode " |
| << output_mode_ |
| << (using_stub_decoder_ ? " with stub video decoder." : "."); |
| } |
| |
| ~VideoDecoderTest() { video_decoder_->Reset(); } |
| |
| void SetUp() override { |
| ASSERT_NE(dmp_reader_.video_codec(), kSbMediaVideoCodecNone); |
| ASSERT_GT(dmp_reader_.number_of_video_buffers(), 0); |
| ASSERT_TRUE(GetVideoInputBuffer(0)->video_sample_info().is_key_frame); |
| |
| SbPlayerOutputMode output_mode = output_mode_; |
| ASSERT_TRUE(VideoDecoder::OutputModeSupported( |
| output_mode, dmp_reader_.video_codec(), kSbDrmSystemInvalid)); |
| |
| PlayerComponents::VideoParameters video_parameters = { |
| &player_, |
| dmp_reader_.video_codec(), |
| kSbDrmSystemInvalid, |
| output_mode, |
| fake_graphics_context_provider_.decoder_target_provider()}; |
| |
| scoped_ptr<PlayerComponents> components; |
| if (using_stub_decoder_) { |
| components = make_scoped_ptr<StubPlayerComponentsImpl>( |
| new StubPlayerComponentsImpl); |
| } else { |
| components = PlayerComponents::Create(); |
| } |
| components->CreateVideoComponents(video_parameters, &video_decoder_, |
| &video_render_algorithm_, |
| &video_renderer_sink_); |
| ASSERT_TRUE(video_decoder_); |
| |
| if (video_renderer_sink_) { |
| video_renderer_sink_->SetRenderCB( |
| std::bind(&VideoDecoderTest::Render, this, _1)); |
| } |
| |
| video_decoder_->Initialize( |
| std::bind(&VideoDecoderTest::OnDecoderStatusUpdate, this, _1, _2), |
| std::bind(&VideoDecoderTest::OnError, this)); |
| } |
| |
| void Render(VideoRendererSink::DrawFrameCB draw_frame_cb) { |
| SB_UNREFERENCED_PARAMETER(draw_frame_cb); |
| } |
| |
| void OnDecoderStatusUpdate(VideoDecoder::Status status, |
| const scoped_refptr<VideoFrame>& frame) { |
| ScopedLock scoped_lock(mutex_); |
| // TODO: Ensure that this is only called during dtor or Reset(). |
| if (status == VideoDecoder::kReleaseAllFrames) { |
| SB_DCHECK(!frame); |
| event_queue_.clear(); |
| decoded_frames_.clear(); |
| return; |
| } else if (status == VideoDecoder::kNeedMoreInput) { |
| event_queue_.push_back(Event(kNeedMoreInput, frame)); |
| } else if (status == VideoDecoder::kBufferFull) { |
| event_queue_.push_back(Event(kBufferFull, frame)); |
| } else { |
| event_queue_.push_back(Event(kError, frame)); |
| } |
| } |
| |
| void OnError() { |
| ScopedLock scoped_lock(mutex_); |
| event_queue_.push_back(Event(kError, NULL)); |
| } |
| |
| #if SB_HAS(GLES2) |
| void AssertInvalidDecodeTarget() { |
| if (output_mode_ == kSbPlayerOutputModeDecodeToTexture && |
| !using_stub_decoder_) { |
| volatile bool is_decode_target_valid = true; |
| fake_graphics_context_provider_.RunOnGlesContextThread([&]() { |
| SbDecodeTarget decode_target = video_decoder_->GetCurrentDecodeTarget(); |
| is_decode_target_valid = SbDecodeTargetIsValid(decode_target); |
| SbDecodeTargetRelease(decode_target); |
| }); |
| ASSERT_FALSE(is_decode_target_valid); |
| } |
| } |
| #endif // SB_HAS(GLES2) |
| |
| protected: |
| enum Status { |
| kNeedMoreInput = VideoDecoder::kNeedMoreInput, |
| kBufferFull = VideoDecoder::kBufferFull, |
| kError, |
| kTimeout |
| }; |
| |
| struct Event { |
| Status status; |
| scoped_refptr<VideoFrame> frame; |
| |
| Event() : status(kNeedMoreInput) {} |
| Event(Status status, scoped_refptr<VideoFrame> frame) |
| : status(status), frame(frame) {} |
| }; |
| |
| // This function is called inside WriteMultipleInputs() whenever an event has |
| // been processed. |
| // |continue_process| will always be a valid pointer and always contains |
| // |true| when calling this callback. The callback can set it to false to |
| // stop further processing. |
| typedef std::function<void(const Event&, bool* continue_process)> EventCB; |
| |
| void WaitForNextEvent( |
| Event* event, |
| SbTimeMonotonic timeout = kDefaultWaitForNextEventTimeOut) { |
| ASSERT_TRUE(event); |
| |
| SbTimeMonotonic start = SbTimeGetMonotonicNow(); |
| while (SbTimeGetMonotonicNow() - start < timeout) { |
| job_queue_.RunUntilIdle(); |
| GetDecodeTargetWhenSupported(); |
| { |
| ScopedLock scoped_lock(mutex_); |
| if (!event_queue_.empty()) { |
| *event = event_queue_.front(); |
| event_queue_.pop_front(); |
| if (event->status == kNeedMoreInput) { |
| need_more_input_ = true; |
| } else if (event->status == kBufferFull) { |
| if (!end_of_stream_written_) { |
| ASSERT_FALSE(need_more_input_); |
| } |
| } |
| return; |
| } |
| } |
| SbThreadSleep(kSbTimeMillisecond); |
| } |
| event->status = kTimeout; |
| } |
| |
| bool HasPendingEvents() { |
| const SbTime kDelay = 5 * kSbTimeMillisecond; |
| SbThreadSleep(kDelay); |
| ScopedLock scoped_lock(mutex_); |
| return !event_queue_.empty(); |
| } |
| |
| void GetDecodeTargetWhenSupported() { |
| #if SB_HAS(GLES2) |
| if (output_mode_ == kSbPlayerOutputModeDecodeToTexture && |
| !using_stub_decoder_) { |
| fake_graphics_context_provider_.RunOnGlesContextThread([&]() { |
| SbDecodeTargetRelease(video_decoder_->GetCurrentDecodeTarget()); |
| }); |
| } |
| #endif // SB_HAS(GLES2) |
| } |
| |
| void AssertValidDecodeTargetWhenSupported() { |
| #if SB_HAS(GLES2) |
| volatile bool is_decode_target_valid = false; |
| if (output_mode_ == kSbPlayerOutputModeDecodeToTexture && |
| !using_stub_decoder_) { |
| fake_graphics_context_provider_.RunOnGlesContextThread([&]() { |
| SbDecodeTarget decode_target = video_decoder_->GetCurrentDecodeTarget(); |
| is_decode_target_valid = SbDecodeTargetIsValid(decode_target); |
| SbDecodeTargetRelease(decode_target); |
| }); |
| ASSERT_TRUE(is_decode_target_valid); |
| } |
| #endif // SB_HAS(GLES2) |
| } |
| |
| // This has to be called when the decoder is just initialized/reseted or when |
| // status is |kNeedMoreInput|. |
| void WriteSingleInput(size_t index) { |
| ASSERT_TRUE(need_more_input_); |
| ASSERT_LT(index, dmp_reader_.number_of_video_buffers()); |
| |
| auto input_buffer = GetVideoInputBuffer(index); |
| { |
| ScopedLock scoped_lock(mutex_); |
| need_more_input_ = false; |
| outstanding_inputs_.insert(input_buffer->timestamp()); |
| } |
| |
| video_decoder_->WriteInputBuffer(input_buffer); |
| } |
| |
| void WriteEndOfStream() { |
| { |
| ScopedLock scoped_lock(mutex_); |
| end_of_stream_written_ = true; |
| } |
| video_decoder_->WriteEndOfStream(); |
| } |
| |
| void WriteMultipleInputs(size_t start_index, |
| size_t number_of_inputs_to_write, |
| EventCB event_cb = EventCB()) { |
| ASSERT_LE(start_index + number_of_inputs_to_write, |
| dmp_reader_.number_of_video_buffers()); |
| |
| ASSERT_NO_FATAL_FAILURE(WriteSingleInput(start_index)); |
| ++start_index; |
| --number_of_inputs_to_write; |
| |
| while (number_of_inputs_to_write > 0) { |
| Event event; |
| ASSERT_NO_FATAL_FAILURE(WaitForNextEvent(&event)); |
| if (event.status == kNeedMoreInput) { |
| ASSERT_NO_FATAL_FAILURE(WriteSingleInput(start_index)); |
| ++start_index; |
| --number_of_inputs_to_write; |
| } else if (event.status == kError || event.status == kTimeout) { |
| // Assume that the caller does't expect an error when |event_cb| isn't |
| // provided. |
| ASSERT_TRUE(event_cb); |
| bool continue_process = true; |
| event_cb(event, &continue_process); |
| ASSERT_FALSE(continue_process); |
| return; |
| } else { |
| ASSERT_EQ(event.status, kBufferFull); |
| } |
| if (event.frame) { |
| ASSERT_FALSE(event.frame->is_end_of_stream()); |
| if (!decoded_frames_.empty()) { |
| ASSERT_LT(decoded_frames_.back()->timestamp(), |
| event.frame->timestamp()); |
| } |
| decoded_frames_.push_back(event.frame); |
| ASSERT_TRUE(AlmostEqualTime(*outstanding_inputs_.begin(), |
| event.frame->timestamp())); |
| outstanding_inputs_.erase(outstanding_inputs_.begin()); |
| } |
| if (event_cb) { |
| bool continue_process = true; |
| event_cb(event, &continue_process); |
| if (!continue_process) { |
| return; |
| } |
| } |
| } |
| } |
| |
| void DrainOutputs(bool* error_occurred = NULL, EventCB event_cb = EventCB()) { |
| if (error_occurred) { |
| *error_occurred = false; |
| } |
| |
| bool end_of_stream_decoded = false; |
| |
| while (!end_of_stream_decoded) { |
| Event event; |
| ASSERT_NO_FATAL_FAILURE(WaitForNextEvent(&event)); |
| if (event.status == kError || event.status == kTimeout) { |
| if (error_occurred) { |
| *error_occurred = true; |
| } else { |
| FAIL(); |
| } |
| return; |
| } |
| if (event.frame) { |
| if (event.frame->is_end_of_stream()) { |
| end_of_stream_decoded = true; |
| if (!outstanding_inputs_.empty()) { |
| if (error_occurred) { |
| *error_occurred = true; |
| } else { |
| // |error_occurred| is NULL indicates that the caller doesn't |
| // expect an error, use the following redundant ASSERT to trigger |
| // a failure. |
| ASSERT_TRUE(outstanding_inputs_.empty()); |
| } |
| } |
| } else { |
| if (!decoded_frames_.empty()) { |
| ASSERT_LT(decoded_frames_.back()->timestamp(), |
| event.frame->timestamp()); |
| } |
| decoded_frames_.push_back(event.frame); |
| ASSERT_TRUE(AlmostEqualTime(*outstanding_inputs_.begin(), |
| event.frame->timestamp())); |
| outstanding_inputs_.erase(outstanding_inputs_.begin()); |
| } |
| } |
| if (event_cb) { |
| bool continue_process = true; |
| event_cb(event, &continue_process); |
| if (!continue_process) { |
| return; |
| } |
| } |
| } |
| } |
| |
| void ResetDecoderAndClearPendingEvents() { |
| video_decoder_->Reset(); |
| ScopedLock scoped_lock(mutex_); |
| event_queue_.clear(); |
| need_more_input_ = true; |
| end_of_stream_written_ = false; |
| outstanding_inputs_.clear(); |
| decoded_frames_.clear(); |
| } |
| |
| scoped_refptr<InputBuffer> GetVideoInputBuffer(size_t index) const { |
| auto video_sample_info = |
| dmp_reader_.GetPlayerSampleInfo(kSbMediaTypeVideo, index); |
| #if SB_API_VERSION >= 11 |
| auto input_buffer = |
| new InputBuffer(DeallocateSampleFunc, NULL, NULL, video_sample_info); |
| #else // SB_API_VERSION >= 11 |
| auto input_buffer = new InputBuffer(kSbMediaTypeVideo, DeallocateSampleFunc, |
| NULL, NULL, video_sample_info, NULL); |
| #endif // SB_API_VERSION >= 11 |
| auto iter = invalid_inputs_.find(index); |
| if (iter != invalid_inputs_.end()) { |
| std::vector<uint8_t> content(input_buffer->size(), iter->second); |
| // Replace the content with invalid data. |
| input_buffer->SetDecryptedContent(content.data(), |
| static_cast<int>(content.size())); |
| } |
| return input_buffer; |
| } |
| |
| void UseInvalidDataForInput(size_t index, uint8_t byte_to_fill) { |
| invalid_inputs_[index] = byte_to_fill; |
| } |
| |
| JobQueue job_queue_; |
| |
| Mutex mutex_; |
| std::deque<Event> event_queue_; |
| |
| // Test parameter filename for the VideoDmpReader to load and test with. |
| const char* test_filename_; |
| |
| // Test parameter for OutputMode. |
| SbPlayerOutputMode output_mode_; |
| |
| // Test parameter for whether or not to use the StubVideoDecoder, or the |
| // platform-specific VideoDecoderImpl. |
| bool using_stub_decoder_; |
| |
| FakeGraphicsContextProvider fake_graphics_context_provider_; |
| VideoDmpReader dmp_reader_; |
| scoped_ptr<VideoDecoder> video_decoder_; |
| |
| bool need_more_input_ = true; |
| std::set<SbTime> outstanding_inputs_; |
| std::deque<scoped_refptr<VideoFrame>> decoded_frames_; |
| |
| private: |
| SbPlayerPrivate player_; |
| scoped_ptr<VideoRenderAlgorithm> video_render_algorithm_; |
| scoped_refptr<VideoRendererSink> video_renderer_sink_; |
| |
| bool end_of_stream_written_ = false; |
| |
| std::map<size_t, uint8_t> invalid_inputs_; |
| }; |
| |
| TEST_P(VideoDecoderTest, PrerollFrameCount) { |
| EXPECT_GT(video_decoder_->GetPrerollFrameCount(), 0); |
| } |
| |
| TEST_P(VideoDecoderTest, MaxNumberOfCachedFrames) { |
| EXPECT_GT(video_decoder_->GetMaxNumberOfCachedFrames(), 1); |
| } |
| |
| TEST_P(VideoDecoderTest, PrerollTimeout) { |
| EXPECT_GE(video_decoder_->GetPrerollTimeout(), 0); |
| } |
| |
| // Ensure that OutputModeSupported() is callable on all combinations. |
| TEST_P(VideoDecoderTest, OutputModeSupported) { |
| SbPlayerOutputMode kOutputModes[] = {kSbPlayerOutputModeDecodeToTexture, |
| kSbPlayerOutputModePunchOut}; |
| SbMediaVideoCodec kVideoCodecs[] = { |
| kSbMediaVideoCodecNone, |
| kSbMediaVideoCodecH264, |
| kSbMediaVideoCodecH265, |
| kSbMediaVideoCodecMpeg2, |
| kSbMediaVideoCodecTheora, |
| kSbMediaVideoCodecVc1, |
| #if SB_API_VERSION < 11 |
| kSbMediaVideoCodecVp10, |
| #else // SB_API_VERSION < 11 |
| kSbMediaVideoCodecAv1, |
| #endif // SB_API_VERSION < 11 |
| kSbMediaVideoCodecVp8, |
| kSbMediaVideoCodecVp9 |
| }; |
| for (auto output_mode : kOutputModes) { |
| for (auto video_codec : kVideoCodecs) { |
| VideoDecoder::OutputModeSupported(output_mode, video_codec, |
| kSbDrmSystemInvalid); |
| } |
| } |
| } |
| |
| #if SB_HAS(GLES2) |
| TEST_P(VideoDecoderTest, GetCurrentDecodeTargetBeforeWriteInputBuffer) { |
| if (output_mode_ == kSbPlayerOutputModeDecodeToTexture) { |
| AssertInvalidDecodeTarget(); |
| } |
| } |
| #endif // SB_HAS(GLES2) |
| |
| TEST_P(VideoDecoderTest, ThreeMoreDecoders) { |
| // Create three more decoders for each supported combinations. |
| const int kDecodersToCreate = 3; |
| |
| scoped_ptr<PlayerComponents> components = PlayerComponents::Create(); |
| |
| SbPlayerOutputMode kOutputModes[] = {kSbPlayerOutputModeDecodeToTexture, |
| kSbPlayerOutputModePunchOut}; |
| SbMediaVideoCodec kVideoCodecs[] = { |
| kSbMediaVideoCodecNone, |
| kSbMediaVideoCodecH264, |
| kSbMediaVideoCodecH265, |
| kSbMediaVideoCodecMpeg2, |
| kSbMediaVideoCodecTheora, |
| kSbMediaVideoCodecVc1, |
| #if SB_API_VERSION < 11 |
| kSbMediaVideoCodecVp10, |
| #else // SB_API_VERSION < 11 |
| kSbMediaVideoCodecAv1, |
| #endif // SB_API_VERSION < 11 |
| kSbMediaVideoCodecVp8, |
| kSbMediaVideoCodecVp9 |
| }; |
| |
| for (auto output_mode : kOutputModes) { |
| for (auto video_codec : kVideoCodecs) { |
| if (VideoDecoder::OutputModeSupported(output_mode, video_codec, |
| kSbDrmSystemInvalid)) { |
| SbPlayerPrivate players[kDecodersToCreate]; |
| scoped_ptr<VideoDecoder> video_decoders[kDecodersToCreate]; |
| scoped_ptr<VideoRenderAlgorithm> |
| video_render_algorithms[kDecodersToCreate]; |
| scoped_refptr<VideoRendererSink> |
| video_renderer_sinks[kDecodersToCreate]; |
| |
| for (int i = 0; i < kDecodersToCreate; ++i) { |
| PlayerComponents::VideoParameters video_parameters = { |
| &players[i], |
| dmp_reader_.video_codec(), |
| kSbDrmSystemInvalid, |
| output_mode, |
| fake_graphics_context_provider_.decoder_target_provider()}; |
| |
| components->CreateVideoComponents( |
| video_parameters, &video_decoders[i], &video_render_algorithms[i], |
| &video_renderer_sinks[i]); |
| ASSERT_TRUE(video_decoders[i]); |
| |
| if (video_renderer_sinks[i]) { |
| video_renderer_sinks[i]->SetRenderCB( |
| std::bind(&VideoDecoderTest::Render, this, _1)); |
| } |
| |
| video_decoders[i]->Initialize( |
| std::bind(&VideoDecoderTest::OnDecoderStatusUpdate, this, _1, _2), |
| std::bind(&VideoDecoderTest::OnError, this)); |
| |
| #if SB_HAS(GLES2) |
| if (output_mode == kSbPlayerOutputModeDecodeToTexture) { |
| AssertInvalidDecodeTarget(); |
| } |
| #endif // SB_HAS(GLES2) |
| } |
| } |
| } |
| } |
| } |
| |
| TEST_P(VideoDecoderTest, SingleInput) { |
| WriteSingleInput(0); |
| WriteEndOfStream(); |
| |
| bool error_occurred = false; |
| ASSERT_NO_FATAL_FAILURE(DrainOutputs( |
| &error_occurred, [=](const Event& event, bool* continue_process) { |
| if (event.frame) { |
| // TODO: On some platforms, decode texture will be ready only after |
| // rendered by renderer, so decode target is not always available |
| // at this point. We should provide a mock renderer and then check |
| // the decode target here with AssertValidDecodeTargetWhenSupported(). |
| } |
| *continue_process = true; |
| })); |
| ASSERT_FALSE(error_occurred); |
| } |
| |
| TEST_P(VideoDecoderTest, SingleInvalidKeyFrame) { |
| UseInvalidDataForInput(0, 0xab); |
| |
| WriteSingleInput(0); |
| WriteEndOfStream(); |
| |
| bool error_occurred = true; |
| ASSERT_NO_FATAL_FAILURE(DrainOutputs(&error_occurred)); |
| // We don't expect the video decoder can always recover from a bad key frame |
| // and to raise an error, but it shouldn't crash or hang. |
| GetDecodeTargetWhenSupported(); |
| } |
| |
| TEST_P(VideoDecoderTest, MultipleValidInputsAfterInvalidKeyFrame) { |
| const size_t kMaxNumberOfInputToWrite = 10; |
| const size_t number_of_input_to_write = |
| std::min(kMaxNumberOfInputToWrite, dmp_reader_.number_of_video_buffers()); |
| |
| UseInvalidDataForInput(0, 0xab); |
| |
| bool error_occurred = false; |
| bool timeout_occurred = false; |
| // Write first few frames. The first one is invalid and the rest are valid. |
| WriteMultipleInputs(0, number_of_input_to_write, |
| [&](const Event& event, bool* continue_process) { |
| if (event.status == kTimeout) { |
| timeout_occurred = true; |
| *continue_process = false; |
| return; |
| } |
| if (event.status == kError) { |
| error_occurred = true; |
| *continue_process = false; |
| return; |
| } |
| *continue_process = event.status != kBufferFull; |
| }); |
| ASSERT_FALSE(timeout_occurred); |
| if (!error_occurred) { |
| GetDecodeTargetWhenSupported(); |
| WriteEndOfStream(); |
| ASSERT_NO_FATAL_FAILURE(DrainOutputs(&error_occurred)); |
| } |
| // We don't expect the video decoder can always recover from a bad key frame |
| // and to raise an error, but it shouldn't crash or hang. |
| GetDecodeTargetWhenSupported(); |
| } |
| |
| TEST_P(VideoDecoderTest, MultipleInvalidInput) { |
| const size_t kMaxNumberOfInputToWrite = 128; |
| const size_t number_of_input_to_write = |
| std::min(kMaxNumberOfInputToWrite, dmp_reader_.number_of_video_buffers()); |
| // Replace the content of the first few input buffers with invalid data. |
| // Every test instance loads its own copy of data so this won't affect other |
| // tests. |
| for (size_t i = 0; i < number_of_input_to_write; ++i) { |
| UseInvalidDataForInput(i, static_cast<uint8_t>(0xab + i)); |
| } |
| |
| bool error_occurred = false; |
| bool timeout_occurred = false; |
| WriteMultipleInputs(0, number_of_input_to_write, |
| [&](const Event& event, bool* continue_process) { |
| if (event.status == kTimeout) { |
| timeout_occurred = true; |
| *continue_process = false; |
| return; |
| } |
| if (event.status == kError) { |
| error_occurred = true; |
| *continue_process = false; |
| return; |
| } |
| |
| *continue_process = event.status != kBufferFull; |
| }); |
| ASSERT_FALSE(timeout_occurred); |
| if (!error_occurred) { |
| GetDecodeTargetWhenSupported(); |
| WriteEndOfStream(); |
| ASSERT_NO_FATAL_FAILURE(DrainOutputs(&error_occurred)); |
| } |
| // We don't expect the video decoder can always recover from a bad key frame |
| // and to raise an error, but it shouldn't crash or hang. |
| GetDecodeTargetWhenSupported(); |
| } |
| |
| TEST_P(VideoDecoderTest, EndOfStreamWithoutAnyInput) { |
| WriteEndOfStream(); |
| ASSERT_NO_FATAL_FAILURE(DrainOutputs()); |
| } |
| |
| TEST_P(VideoDecoderTest, ResetBeforeInput) { |
| EXPECT_FALSE(HasPendingEvents()); |
| ResetDecoderAndClearPendingEvents(); |
| EXPECT_FALSE(HasPendingEvents()); |
| |
| WriteSingleInput(0); |
| WriteEndOfStream(); |
| ASSERT_NO_FATAL_FAILURE(DrainOutputs()); |
| } |
| |
| TEST_P(VideoDecoderTest, ResetAfterInput) { |
| const size_t kMaxInputToWrite = 10; |
| bool error_occurred = false; |
| WriteMultipleInputs( |
| 0, kMaxInputToWrite, [&](const Event& event, bool* continue_process) { |
| if (event.status == kTimeout || event.status == kError) { |
| error_occurred = true; |
| *continue_process = false; |
| return; |
| } |
| *continue_process = event.status != kBufferFull; |
| }); |
| ASSERT_FALSE(error_occurred); |
| ResetDecoderAndClearPendingEvents(); |
| EXPECT_FALSE(HasPendingEvents()); |
| } |
| |
| TEST_P(VideoDecoderTest, MultipleResets) { |
| for (int max_inputs = 1; max_inputs < 10; ++max_inputs) { |
| bool error_occurred = false; |
| WriteMultipleInputs( |
| 0, max_inputs, [&](const Event& event, bool* continue_process) { |
| if (event.status == kTimeout || event.status == kError) { |
| error_occurred = true; |
| *continue_process = false; |
| return; |
| } |
| *continue_process = event.status != kBufferFull; |
| }); |
| ASSERT_FALSE(error_occurred); |
| ResetDecoderAndClearPendingEvents(); |
| EXPECT_FALSE(HasPendingEvents()); |
| WriteSingleInput(0); |
| WriteEndOfStream(); |
| ASSERT_NO_FATAL_FAILURE(DrainOutputs()); |
| ResetDecoderAndClearPendingEvents(); |
| EXPECT_FALSE(HasPendingEvents()); |
| } |
| } |
| |
| TEST_P(VideoDecoderTest, MultipleInputs) { |
| const size_t kMaxNumberOfExpectedDecodedFrames = 5; |
| const size_t number_of_expected_decoded_frames = std::min( |
| kMaxNumberOfExpectedDecodedFrames, dmp_reader_.number_of_video_buffers()); |
| size_t frames_decoded = 0; |
| bool error_occurred = false; |
| ASSERT_NO_FATAL_FAILURE(WriteMultipleInputs( |
| 0, dmp_reader_.number_of_video_buffers(), |
| [&](const Event& event, bool* continue_process) { |
| if (event.status == kTimeout || event.status == kError) { |
| error_occurred = true; |
| *continue_process = false; |
| return; |
| } |
| frames_decoded += decoded_frames_.size(); |
| decoded_frames_.clear(); |
| *continue_process = frames_decoded < number_of_expected_decoded_frames; |
| })); |
| ASSERT_FALSE(error_occurred); |
| if (frames_decoded < number_of_expected_decoded_frames) { |
| WriteEndOfStream(); |
| ASSERT_NO_FATAL_FAILURE(DrainOutputs()); |
| } |
| } |
| |
| TEST_P(VideoDecoderTest, Preroll) { |
| SbTimeMonotonic start = SbTimeGetMonotonicNow(); |
| SbTime preroll_timeout = video_decoder_->GetPrerollTimeout(); |
| bool error_occurred = false; |
| ASSERT_NO_FATAL_FAILURE(WriteMultipleInputs( |
| 0, dmp_reader_.number_of_video_buffers(), |
| [&](const Event& event, bool* continue_process) { |
| if (event.status == kError) { |
| error_occurred = true; |
| *continue_process = false; |
| return; |
| } |
| if (decoded_frames_.size() >= video_decoder_->GetPrerollFrameCount()) { |
| *continue_process = false; |
| return; |
| } |
| if (SbTimeGetMonotonicNow() - start >= preroll_timeout) { |
| // After preroll timeout, we should get at least 1 decoded frame. |
| ASSERT_GT(decoded_frames_.size(), 0); |
| *continue_process = false; |
| return; |
| } |
| *continue_process = true; |
| return; |
| })); |
| ASSERT_FALSE(error_occurred); |
| } |
| |
| TEST_P(VideoDecoderTest, HoldFramesUntilFull) { |
| bool error_occurred = false; |
| ASSERT_NO_FATAL_FAILURE(WriteMultipleInputs( |
| 0, dmp_reader_.number_of_video_buffers(), |
| [&](const Event& event, bool* continue_process) { |
| if (event.status == kTimeout || event.status == kError) { |
| error_occurred = true; |
| *continue_process = false; |
| return; |
| } |
| *continue_process = decoded_frames_.size() < |
| video_decoder_->GetMaxNumberOfCachedFrames(); |
| })); |
| ASSERT_FALSE(error_occurred); |
| WriteEndOfStream(); |
| if (decoded_frames_.size() >= video_decoder_->GetMaxNumberOfCachedFrames()) { |
| return; |
| } |
| ASSERT_NO_FATAL_FAILURE(DrainOutputs( |
| &error_occurred, [=](const Event& event, bool* continue_process) { |
| *continue_process = decoded_frames_.size() < |
| video_decoder_->GetMaxNumberOfCachedFrames(); |
| })); |
| ASSERT_FALSE(error_occurred); |
| } |
| |
| TEST_P(VideoDecoderTest, DecodeFullGOP) { |
| int gop_size = 1; |
| while (gop_size < dmp_reader_.number_of_video_buffers()) { |
| if (GetVideoInputBuffer(gop_size)->video_sample_info().is_key_frame) { |
| break; |
| } |
| ++gop_size; |
| } |
| bool error_occurred = false; |
| ASSERT_NO_FATAL_FAILURE(WriteMultipleInputs( |
| 0, gop_size, [&](const Event& event, bool* continue_process) { |
| if (event.status == kTimeout || event.status == kError) { |
| error_occurred = true; |
| *continue_process = false; |
| return; |
| } |
| // Keep 1 decoded frame, assuming it's used by renderer. |
| while (decoded_frames_.size() > 1) { |
| decoded_frames_.pop_front(); |
| } |
| *continue_process = true; |
| })); |
| ASSERT_FALSE(error_occurred); |
| WriteEndOfStream(); |
| |
| ASSERT_NO_FATAL_FAILURE(DrainOutputs( |
| &error_occurred, [=](const Event& event, bool* continue_process) { |
| // Keep 1 decoded frame, assuming it's used by renderer. |
| while (decoded_frames_.size() > 1) { |
| decoded_frames_.pop_front(); |
| } |
| *continue_process = true; |
| })); |
| ASSERT_FALSE(error_occurred); |
| } |
| |
| std::vector<TestParam> GetSupportedTests() { |
| SbPlayerOutputMode kOutputModes[] = {kSbPlayerOutputModeDecodeToTexture, |
| kSbPlayerOutputModePunchOut}; |
| |
| const char* kFilenames[] = {"beneath_the_canopy_137_avc.dmp", |
| "beneath_the_canopy_248_vp9.dmp"}; |
| |
| static std::vector<TestParam> test_params; |
| |
| if (!test_params.empty()) { |
| return test_params; |
| } |
| |
| for (auto filename : kFilenames) { |
| VideoDmpReader dmp_reader(ResolveTestFileName(filename).c_str()); |
| SB_DCHECK(dmp_reader.number_of_video_buffers() > 0); |
| |
| for (auto output_mode : kOutputModes) { |
| if (!VideoDecoder::OutputModeSupported( |
| output_mode, dmp_reader.video_codec(), kSbDrmSystemInvalid)) { |
| continue; |
| } |
| |
| const auto& video_sample_info = |
| dmp_reader.GetPlayerSampleInfo(kSbMediaTypeVideo, 0) |
| .video_sample_info; |
| |
| if (SbMediaIsVideoSupported( |
| dmp_reader.video_codec(), |
| #if SB_HAS(MEDIA_IS_VIDEO_SUPPORTED_REFINEMENT) |
| -1, -1, 8, kSbMediaPrimaryIdUnspecified, |
| kSbMediaTransferIdUnspecified, kSbMediaMatrixIdUnspecified, |
| #endif // SB_HAS(MEDIA_IS_VIDEO_SUPPORTED_REFINEMENT) |
| #if SB_API_VERSION >= 11 |
| video_sample_info.frame_width, video_sample_info.frame_height, |
| #else // SB_API_VERSION >= 11 |
| video_sample_info->frame_width, video_sample_info->frame_height, |
| #endif // SB_API_VERSION >= 11 |
| dmp_reader.video_bitrate(), dmp_reader.video_fps() |
| #if SB_API_VERSION >= 10 |
| , |
| false |
| #endif // SB_API_VERSION >= 10 |
| )) { |
| test_params.push_back({output_mode, filename}); |
| } |
| } |
| } |
| |
| SB_DCHECK(!test_params.empty()); |
| return test_params; |
| } |
| |
| INSTANTIATE_TEST_CASE_P(VideoDecoderTests, |
| VideoDecoderTest, |
| Combine(ValuesIn(GetSupportedTests()), Bool())); |
| |
| } // namespace |
| } // namespace testing |
| } // namespace filter |
| } // namespace player |
| } // namespace starboard |
| } // namespace shared |
| } // namespace starboard |
| #endif // SB_HAS(PLAYER_FILTER_TESTS) |