blob: 551a0ad701268a9bb3016633ab45b920d03abe73 [file] [log] [blame]
// Copyright 2019 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 <algorithm>
#include <cmath>
#include <deque>
#include <functional>
#include <memory>
#include <numeric>
#include <queue>
#include <string>
#include "starboard/common/mutex.h"
#include "starboard/common/scoped_ptr.h"
#include "starboard/configuration_constants.h"
#include "starboard/directory.h"
#include "starboard/shared/starboard/media/media_support_internal.h"
#include "starboard/shared/starboard/player/filter/audio_decoder_internal.h"
#include "starboard/shared/starboard/player/filter/player_components.h"
#include "starboard/shared/starboard/player/filter/stub_player_components_factory.h"
#include "starboard/shared/starboard/player/filter/testing/test_util.h"
#include "starboard/shared/starboard/player/job_queue.h"
#include "starboard/shared/starboard/player/video_dmp_reader.h"
#include "starboard/thread.h"
#include "testing/gtest/include/gtest/gtest.h"
// TODO: Implement AudioDecoderMock and refactor the test accordingly.
namespace starboard {
namespace shared {
namespace starboard {
namespace player {
namespace filter {
namespace testing {
namespace {
using std::string;
using std::vector;
using ::testing::Bool;
using ::testing::Combine;
using ::testing::ValuesIn;
using video_dmp::VideoDmpReader;
const SbTimeMonotonic kWaitForNextEventTimeOut = 5 * kSbTimeSecond;
scoped_refptr<InputBuffer> GetAudioInputBuffer(VideoDmpReader* dmp_reader,
size_t index) {
SB_DCHECK(dmp_reader);
auto player_sample_info =
dmp_reader->GetPlayerSampleInfo(kSbMediaTypeAudio, index);
return new InputBuffer(StubDeallocateSampleFunc, NULL, NULL,
player_sample_info);
}
string GetTestInputDirectory() {
const size_t kPathSize = kSbFileMaxPath + 1;
std::vector<char> content_path(kPathSize);
SB_CHECK(SbSystemGetPath(kSbSystemPathContentDirectory, content_path.data(),
kPathSize));
string directory_path =
string(content_path.data()) + kSbFileSepChar + "test" + kSbFileSepChar +
"starboard" + kSbFileSepChar + "shared" + kSbFileSepChar + "starboard" +
kSbFileSepChar + "player" + kSbFileSepChar + "testdata";
SB_CHECK(SbDirectoryCanOpen(directory_path.c_str()))
<< "Cannot open directory " << directory_path;
return directory_path;
}
string ResolveTestFileName(const char* filename) {
return GetTestInputDirectory() + kSbFileSepChar + filename;
}
class AdaptiveAudioDecoderTest
: public ::testing::TestWithParam<std::tuple<vector<const char*>, bool>> {
protected:
enum Event { kConsumed, kOutput, kError };
AdaptiveAudioDecoderTest()
: test_filenames_(std::get<0>(GetParam())),
using_stub_decoder_(std::get<1>(GetParam())) {
for (auto filename : test_filenames_) {
dmp_readers_.emplace_back(
new VideoDmpReader(ResolveTestFileName(filename).c_str(),
VideoDmpReader::kEnableReadOnDemand));
}
auto accumulate_operation = [](string accumulated, const char* str) {
if (!accumulated.length()) {
return string(str);
}
return std::move(accumulated) + "->" + str;
};
string description =
std::accumulate(test_filenames_.begin(), test_filenames_.end(),
string(), accumulate_operation);
SB_LOG(INFO) << "Testing: " << description
<< (using_stub_decoder_ ? " (with stub decoder)." : ".");
}
void SetUp() override {
ASSERT_GT(dmp_readers_.size(), 0);
for (auto& dmp_reader : dmp_readers_) {
ASSERT_NE(dmp_reader->audio_codec(), kSbMediaAudioCodecNone);
ASSERT_GT(dmp_reader->number_of_audio_buffers(), 0);
}
scoped_ptr<AudioRendererSink> audio_renderer_sink;
ASSERT_TRUE(CreateAudioComponents(using_stub_decoder_,
dmp_readers_[0]->audio_codec(),
dmp_readers_[0]->audio_sample_info(),
&audio_decoder_, &audio_renderer_sink));
ASSERT_TRUE(audio_decoder_);
audio_decoder_->Initialize(
std::bind(&AdaptiveAudioDecoderTest::OnOutput, this),
std::bind(&AdaptiveAudioDecoderTest::OnError, this));
}
void WriteSingleInput(VideoDmpReader* dmp_reader, size_t buffer_index) {
SB_DCHECK(dmp_reader);
ASSERT_TRUE(can_accept_more_input_);
ASSERT_LT(buffer_index, dmp_reader->number_of_audio_buffers());
can_accept_more_input_ = false;
audio_decoder_->Decode(
{GetAudioInputBuffer(dmp_reader, buffer_index)},
std::bind(&AdaptiveAudioDecoderTest::OnConsumed, this));
}
void WriteMultipleInputs(VideoDmpReader* dmp_reader,
size_t buffer_start_index,
size_t number_of_inputs_to_write) {
SB_DCHECK(dmp_reader);
ASSERT_LT(buffer_start_index + number_of_inputs_to_write,
dmp_reader->number_of_audio_buffers());
while (number_of_inputs_to_write > 0) {
ASSERT_NO_FATAL_FAILURE(WaitAndProcessUntilAcceptInput());
ASSERT_NO_FATAL_FAILURE(WriteSingleInput(dmp_reader, buffer_start_index));
++buffer_start_index;
--number_of_inputs_to_write;
}
}
void WriteEndOfStream() {
ASSERT_NO_FATAL_FAILURE(WaitAndProcessUntilAcceptInput());
audio_decoder_->WriteEndOfStream();
}
void WaitAndProcessNextEvent(Event* event) {
SbTimeMonotonic start = SbTimeGetMonotonicNow();
while (SbTimeGetMonotonicNow() - start < kWaitForNextEventTimeOut) {
job_queue_.RunUntilIdle();
{
ScopedLock scoped_lock(event_queue_mutex_);
if (!event_queue_.empty()) {
*event = event_queue_.front();
event_queue_.pop_front();
ProcessEvent(*event);
return;
}
}
SbThreadSleep(kSbTimeMillisecond);
}
*event = kError;
FAIL();
}
void WaitAndProcessUntilAcceptInput() {
while (!can_accept_more_input_) {
Event event = kError;
ASSERT_NO_FATAL_FAILURE(WaitAndProcessNextEvent(&event));
if (event != kConsumed) {
ASSERT_EQ(kOutput, event);
ASSERT_TRUE(last_decoded_audio_);
}
}
}
void DrainOutputs() {
while (!last_decoded_audio_ || !last_decoded_audio_->is_end_of_stream()) {
Event event = kError;
ASSERT_NO_FATAL_FAILURE(WaitAndProcessNextEvent(&event));
if (event != kConsumed) {
ASSERT_EQ(kOutput, event);
ASSERT_TRUE(last_decoded_audio_);
}
}
}
void AssertExpectedAndOutputFramesMatch(int expected_output_frames) {
if (using_stub_decoder_) {
// The number of output frames is not applicable in the case of the
// StubAudioDecoder, because it is not actually doing any decoding work.
return;
}
ASSERT_LE(abs(expected_output_frames - num_of_output_frames_),
dmp_readers_.size());
}
vector<std::unique_ptr<VideoDmpReader>> dmp_readers_;
scoped_refptr<DecodedAudio> last_decoded_audio_;
int num_of_output_frames_ = 0;
int output_sample_rate_;
bool first_output_received_ = false;
private:
void OnOutput() {
ScopedLock scoped_lock(event_queue_mutex_);
event_queue_.push_back(kOutput);
}
void OnError() {
ScopedLock scoped_lock(event_queue_mutex_);
event_queue_.push_back(kError);
}
void OnConsumed() {
ScopedLock scoped_lock(event_queue_mutex_);
event_queue_.push_back(kConsumed);
}
void ProcessEvent(Event event) {
switch (event) {
case kConsumed: {
can_accept_more_input_ = true;
break;
}
case kOutput: {
ReadFromDecoder();
break;
}
case kError: {
break;
}
default:
SB_NOTREACHED();
}
}
void ReadFromDecoder() {
int samples_per_second;
scoped_refptr<DecodedAudio> decoded_audio =
audio_decoder_->Read(&samples_per_second);
ASSERT_TRUE(decoded_audio);
if (first_output_received_) {
ASSERT_EQ(output_sample_rate_, samples_per_second);
} else {
output_sample_rate_ = samples_per_second;
first_output_received_ = true;
}
if (decoded_audio->is_end_of_stream()) {
last_decoded_audio_ = decoded_audio;
return;
}
// TODO: fix resampler timestamp issue.
// if (last_decoded_audio_) {
// ASSERT_LT(last_decoded_audio_->timestamp(),
// decoded_audio->timestamp());
// }
last_decoded_audio_ = decoded_audio;
num_of_output_frames_ += last_decoded_audio_->frames();
}
// Test parameter for the filenames to load with the VideoDmpReader.
std::vector<const char*> test_filenames_;
// Test parameter to configure whether the test is run with the
// StubAudioDecoder, or the platform-specific AudioDecoderImpl.
bool using_stub_decoder_;
JobQueue job_queue_;
scoped_ptr<AudioDecoder> audio_decoder_;
Mutex event_queue_mutex_;
std::deque<Event> event_queue_;
bool can_accept_more_input_ = true;
};
std::string GetAdaptiveAudioDecoderTestConfigName(
::testing::TestParamInfo<std::tuple<vector<const char*>, bool>> info) {
std::vector<const char*> filenames(std::get<0>(info.param));
bool using_stub_decoder = std::get<1>(info.param);
std::string config_name;
for (auto name : filenames) {
config_name += std::string(name) + "__to__";
}
if (!config_name.empty()) {
// Remove trailing "__to__".
config_name.erase(config_name.end() - 6, config_name.end());
std::replace(config_name.begin(), config_name.end(), '.', '_');
if (using_stub_decoder) {
config_name += "__stub";
}
}
return config_name;
}
TEST_P(AdaptiveAudioDecoderTest, SingleInput) {
SbTime playing_duration = 0;
// Skip buffer 0, as the difference between first and second opus buffer
// timestamp is a little larger than it should be.
size_t buffer_index = 1;
const size_t kBuffersToWrite = 1;
for (auto& dmp_reader : dmp_readers_) {
SB_DCHECK(dmp_reader);
ASSERT_NO_FATAL_FAILURE(
WriteMultipleInputs(dmp_reader.get(), buffer_index, kBuffersToWrite));
auto input_buffer = GetAudioInputBuffer(dmp_reader.get(), buffer_index);
SbTime input_timestamp = input_buffer->timestamp();
buffer_index += kBuffersToWrite;
// Use next buffer here, need to make sure dmp file has enough buffers.
SB_DCHECK(dmp_reader->number_of_audio_buffers() > buffer_index);
auto next_input_buffer =
GetAudioInputBuffer(dmp_reader.get(), buffer_index);
SbTime next_timestamp = next_input_buffer->timestamp();
playing_duration += next_timestamp - input_timestamp;
}
ASSERT_NO_FATAL_FAILURE(WriteEndOfStream());
ASSERT_NO_FATAL_FAILURE(DrainOutputs());
ASSERT_EQ(true, first_output_received_);
ASSERT_NE(0, output_sample_rate_);
int expected_output_frames = playing_duration * output_sample_rate_ /
static_cast<double>(kSbTimeSecond);
// The |num_of_output_frames_| may not accurately match
// |expected_output_frames|. Each time to switch decoder, it may have one
// sample difference in output due to integer conversion. The total difference
// should not exceed the length of |dmp_readers_|.
AssertExpectedAndOutputFramesMatch(expected_output_frames);
}
TEST_P(AdaptiveAudioDecoderTest, MultipleInput) {
SbTime playing_duration = 0;
// Skip buffer 0, as the difference between first and second opus buffer
// timestamp is a little larger than it should be.
size_t buffer_index = 1;
const size_t kBuffersToWrite = 5;
for (auto& dmp_reader : dmp_readers_) {
SB_DCHECK(dmp_reader);
ASSERT_NO_FATAL_FAILURE(
WriteMultipleInputs(dmp_reader.get(), buffer_index, kBuffersToWrite));
auto input_buffer = GetAudioInputBuffer(dmp_reader.get(), buffer_index);
SbTime input_timestamp = input_buffer->timestamp();
buffer_index += kBuffersToWrite;
// Use next buffer here, need to make sure dmp file has enough buffers.
SB_DCHECK(dmp_reader->number_of_audio_buffers() > buffer_index);
auto next_input_buffer =
GetAudioInputBuffer(dmp_reader.get(), buffer_index);
SbTime next_timestamp = next_input_buffer->timestamp();
playing_duration += next_timestamp - input_timestamp;
}
ASSERT_NO_FATAL_FAILURE(WriteEndOfStream());
ASSERT_NO_FATAL_FAILURE(DrainOutputs());
ASSERT_EQ(true, first_output_received_);
ASSERT_NE(0, output_sample_rate_);
int expected_output_frames = playing_duration * output_sample_rate_ /
static_cast<double>(kSbTimeSecond);
// The |num_of_output_frames_| may not accurately match
// |expected_output_frames|. Each time to switch decoder, it may have one
// sample difference in output due to integer conversion. The total difference
// should not exceed the length of |dmp_readers_|.
AssertExpectedAndOutputFramesMatch(expected_output_frames);
}
vector<vector<const char*>> GetSupportedTests() {
static vector<vector<const char*>> test_params;
if (!test_params.empty()) {
return test_params;
}
vector<const char*> supported_files =
GetSupportedAudioTestFiles(kExcludeHeaac, 6, "audiopassthrough=false");
// Generate test cases. For example, we have |supported_files| [A, B, C].
// Add tests A->A, A->B, A->C, B->A, B->B, B->C, C->A, C->B and C->C.
for (int i = 0; i < supported_files.size(); i++) {
for (int j = 0; j < supported_files.size(); j++) {
test_params.push_back({supported_files[i], supported_files[j]});
}
}
// Add tests A->B->C and C->B->A.
if (supported_files.size() > 2) {
test_params.push_back(supported_files);
test_params.emplace_back(supported_files.rbegin(), supported_files.rend());
}
// Add tests A->B->C->A->B->C and C->B->A->C->B->A.
test_params.push_back(supported_files);
test_params.back().insert(test_params.back().end(), supported_files.begin(),
supported_files.end());
test_params.emplace_back(test_params.back().rbegin(),
test_params.back().rend());
SB_LOG_IF(INFO, test_params.empty())
<< "Test params for AdaptiveAudioDecoderTests is empty.";
return test_params;
}
INSTANTIATE_TEST_CASE_P(AdaptiveAudioDecoderTests,
AdaptiveAudioDecoderTest,
Combine(ValuesIn(GetSupportedTests()), Bool()),
GetAdaptiveAudioDecoderTestConfigName);
} // namespace
} // namespace testing
} // namespace filter
} // namespace player
} // namespace starboard
} // namespace shared
} // namespace starboard