blob: 0a67f32a4adcfe0ee23ead2b73a1c74169e2df0c [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 <cmath>
#include <functional>
#include <numeric>
#include "starboard/common/mutex.h"
#include "starboard/common/scoped_ptr.h"
#include "starboard/configuration_constants.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_impl.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.
#if SB_HAS(PLAYER_FILTER_TESTS) && \
SB_API_VERSION >= 11
namespace starboard {
namespace shared {
namespace starboard {
namespace player {
namespace filter {
namespace testing {
namespace {
using ::testing::Bool;
using ::testing::Combine;
using ::testing::ValuesIn;
using std::vector;
using std::string;
using video_dmp::VideoDmpReader;
const SbTimeMonotonic kWaitForNextEventTimeOut = 5 * kSbTimeSecond;
void DeallocateSampleFunc(SbPlayer player,
void* context,
const void* sample_buffer) {
SB_UNREFERENCED_PARAMETER(player);
SB_UNREFERENCED_PARAMETER(context);
SB_UNREFERENCED_PARAMETER(sample_buffer);
}
scoped_refptr<InputBuffer> GetAudioInputBuffer(const VideoDmpReader& dmp_reader,
size_t index) {
auto player_sample_info =
dmp_reader.GetPlayerSampleInfo(kSbMediaTypeAudio, index);
#if SB_API_VERSION >= 11
return new InputBuffer(DeallocateSampleFunc, NULL, NULL, player_sample_info);
#else // SB_API_VERSION >= 11
SbMediaAudioSampleInfo audio_sample_info =
dmp_reader.GetAudioSampleInfo(index);
return new InputBuffer(kSbMediaTypeAudio, DeallocateSampleFunc, NULL, NULL,
player_sample_info, &audio_sample_info);
#endif // SB_API_VERSION >= 11
}
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;
}
// TODO: Avoid reading same dmp file repeatly.
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()));
}
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);
}
PlayerComponents::AudioParameters audio_parameters = {
dmp_readers_[0]->audio_codec(), dmp_readers_[0]->audio_sample_info(),
kSbDrmSystemInvalid};
scoped_ptr<AudioRendererSink> audio_renderer_sink;
scoped_ptr<PlayerComponents> components;
if (using_stub_decoder_) {
components = make_scoped_ptr<StubPlayerComponentsImpl>(
new StubPlayerComponentsImpl);
} else {
components = PlayerComponents::Create();
}
std::string error_message;
ASSERT_TRUE(components->CreateAudioComponents(
audio_parameters, &audio_decoder_, &audio_renderer_sink,
&error_message));
ASSERT_TRUE(audio_decoder_);
audio_decoder_->Initialize(
std::bind(&AdaptiveAudioDecoderTest::OnOutput, this),
std::bind(&AdaptiveAudioDecoderTest::OnError, this));
}
void WriteSingleInput(const VideoDmpReader& dmp_reader, size_t buffer_index) {
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(const VideoDmpReader& dmp_reader,
size_t buffer_start_index,
size_t number_of_inputs_to_write) {
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;
};
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, buffer_index, kBuffersToWrite));
auto input_buffer = GetAudioInputBuffer(*dmp_reader, 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, 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, buffer_index, kBuffersToWrite));
auto input_buffer = GetAudioInputBuffer(*dmp_reader, 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, 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 ouput due to integer conversion. The total difference
// should not exceed the length of |dmp_readers_|.
AssertExpectedAndOutputFramesMatch(expected_output_frames);
}
vector<vector<const char*>> GetSupportedTests() {
// beneath_the_canopy_aac_stereo.dmp
// codec: kSbMediaAudioCodecAac
// sampling rate: 44.1k
// frames per AU: 1024
// beneath_the_canopy_opus_stereo.dmp
// codec: kSbMediaAudioCodecOpus
// sampling rate: 48.0k
// frames per AU: 960
const char* kFilenames[] = {"beneath_the_canopy_aac_stereo.dmp",
"beneath_the_canopy_aac_5_1.dmp",
"beneath_the_canopy_aac_mono.dmp",
"beneath_the_canopy_opus_5_1.dmp",
"beneath_the_canopy_opus_stereo.dmp",
"beneath_the_canopy_opus_mono.dmp",
"sintel_329_ec3.dmp",
"sintel_381_ac3.dmp"};
static vector<vector<const char*>> test_params;
if (!test_params.empty()) {
return test_params;
}
vector<const char*> supported_files;
for (auto filename : kFilenames) {
VideoDmpReader dmp_reader(ResolveTestFileName(filename).c_str());
SB_DCHECK(dmp_reader.number_of_audio_buffers() > 0);
if (SbMediaIsAudioSupported(dmp_reader.audio_codec(),
dmp_reader.audio_bitrate())) {
supported_files.push_back(filename);
}
}
// 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 AdaptiveAudioDecodeTests is empty.";
return test_params;
}
INSTANTIATE_TEST_CASE_P(AdaptiveAudioDecoderTests,
AdaptiveAudioDecoderTest,
Combine(ValuesIn(GetSupportedTests()), Bool()));
} // namespace
} // namespace testing
} // namespace filter
} // namespace player
} // namespace starboard
} // namespace shared
} // namespace starboard
#endif // SB_HAS(PLAYER_FILTER_TESTS) &&
// SB_API_VERSION >= 11