blob: d33c77704b965f89479f843a9c951013d1b3fb26 [file] [log] [blame]
// Copyright 2018 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <getopt.h>
#include <inttypes.h>
#include <algorithm>
#include <cmath>
#include <fstream>
#include <functional>
#include <map>
#include <memory>
#include <string>
#include <vector>
#include <gtest/gtest.h>
#include <utils/Log.h>
#include "arc/codec-test/common.h"
#include "arc/codec-test/mediacodec_encoder.h"
namespace android {
// Environment to store test stream data for all test cases.
class ArcVideoEncoderTestEnvironment;
namespace {
// Default initial bitrate.
const unsigned int kDefaultBitrate = 2000000;
// Default ratio of requested_subsequent_bitrate_ to initial_bitrate
// (see test parameters below) if one is not provided.
const double kDefaultSubsequentBitrateRatio = 2.0;
// Default initial framerate.
const unsigned int kDefaultFramerate = 30;
// Default ratio of requested_subsequent_framerate_ to initial_framerate
// (see test parameters below) if one is not provided.
const double kDefaultSubsequentFramerateRatio = 0.1;
// Tolerance factor for how encoded bitrate can differ from requested bitrate.
const double kBitrateTolerance = 0.1;
// The minimum number of encoded frames. If the frame number of the input stream
// is less than this value, then we circularly encode the input stream.
constexpr size_t kMinNumEncodedFrames = 300;
// The percentiles to measure for encode latency.
constexpr int kLoggedLatencyPercentiles[] = {50, 75, 95};
#if ANDROID_VERSION < 0x0900 // Android 9.0 (Pie)
constexpr int AMEDIACODEC_BUFFER_FLAG_CODEC_CONFIG = 2;
#endif
ArcVideoEncoderTestEnvironment* g_env;
} // namespace
// Store the arguments passed from command line.
struct CmdlineArgs {
std::string test_stream_data;
bool run_at_fps = false;
size_t num_encoded_frames = 0;
};
class ArcVideoEncoderTestEnvironment : public testing::Environment {
public:
explicit ArcVideoEncoderTestEnvironment(const CmdlineArgs& args)
: args_(args) {}
void SetUp() override {
ParseTestStreamData();
}
// The syntax of test stream is:
// "input_file_path:width:height:profile:output_file_path:requested_bitrate
// :requested_framerate:requestedSubsequentBitrate
// :requestedSubsequentFramerate:pixelFormat"
// - |input_file_path| is YUV raw stream. Its format must be |pixelFormat|
// (see http://www.fourcc.org/yuv.php#IYUV).
// - |width| and |height| are in pixels.
// - |profile| to encode into (values of VideoCodecProfile).
// NOTE: Only H264PROFILE_MAIN(1) is supported. Now we ignore this value.
// - |output_file_path| filename to save the encoded stream to (optional).
// The format for H264 is Annex-B byte stream.
// - |requested_bitrate| requested bitrate in bits per second.
// Bitrate is only forced for tests that test bitrate.
// - |requested_framerate| requested initial framerate.
// - |requestedSubsequentBitrate| bitrate to switch to in the middle of the
// stream. NOTE: This value is not supported yet.
// - |requestedSubsequentFramerate| framerate to switch to in the middle of
// the stream. NOTE: This value is not supported yet.
// - |pixelFormat| is the VideoPixelFormat of |input_file_path|.
// NOTE: Only PIXEL_FORMAT_I420 is supported. Now we just ignore this value.
void ParseTestStreamData() {
std::vector<std::string> fields = SplitString(args_.test_stream_data, ':');
ALOG_ASSERT(fields.size() >= 3U,
"The fields of test_stream_data is not enough: %s",
args_.test_stream_data.c_str());
ALOG_ASSERT(fields.size() <= 10U,
"The fields of test_stream_data is too much: %s",
args_.test_stream_data.c_str());
input_file_path_ = fields[0];
int width = std::stoi(fields[1]);
int height = std::stoi(fields[2]);
visible_size_ = Size(width, height);
ASSERT_FALSE(visible_size_.IsEmpty());
if (fields.size() >= 4 && !fields[3].empty()) {
int profile = stoi(fields[3]);
if (profile != 1)
printf("[WARN] Only H264PROFILE_MAIN(1) is supported.\n");
}
if (fields.size() >= 5 && !fields[4].empty()) {
output_file_path_ = fields[4];
}
if (fields.size() >= 6 && !fields[5].empty()) {
requested_bitrate_ = stoi(fields[5]);
ASSERT_GT(requested_bitrate_, 0);
} else {
requested_bitrate_ = kDefaultBitrate;
}
if (fields.size() >= 7 && !fields[6].empty()) {
requested_framerate_ = std::stoi(fields[6]);
ASSERT_GT(requested_framerate_, 0);
} else {
requested_framerate_ = kDefaultFramerate;
}
if (fields.size() >= 8 && !fields[7].empty()) {
requested_subsequent_bitrate_ = std::stoi(fields[7]);
ASSERT_GT(requested_subsequent_bitrate_, 0);
} else {
requested_subsequent_bitrate_ =
requested_bitrate_ * kDefaultSubsequentBitrateRatio;
}
if (fields.size() >= 9 && !fields[8].empty()) {
requested_subsequent_framerate_ = std::stoi(fields[8]);
ASSERT_GT(requested_subsequent_framerate_, 0);
} else {
requested_subsequent_framerate_ =
requested_framerate_ * kDefaultSubsequentFramerateRatio;
}
if (fields.size() >= 10 && !fields[9].empty()) {
int format = std::stoi(fields[9]);
if (format != 1 /* PIXEL_FORMAT_I420 */)
printf("[WARN] Only I420 is suppported.\n");
}
}
Size visible_size() const { return visible_size_; }
std::string input_file_path() const { return input_file_path_; }
std::string output_file_path() const { return output_file_path_; }
int requested_bitrate() const { return requested_bitrate_; }
int requested_framerate() const { return requested_framerate_; }
int requested_subsequent_bitrate() const {
return requested_subsequent_bitrate_;
}
int requested_subsequent_framerate() const {
return requested_subsequent_framerate_;
}
bool run_at_fps() const { return args_.run_at_fps; }
size_t num_encoded_frames() const { return args_.num_encoded_frames; }
private:
const CmdlineArgs args_;
Size visible_size_;
std::string input_file_path_;
std::string output_file_path_;
int requested_bitrate_;
int requested_framerate_;
int requested_subsequent_bitrate_;
int requested_subsequent_framerate_;
};
class ArcVideoEncoderE2ETest : public testing::Test {
public:
// Callback functions of getting output buffers from encoder.
void WriteOutputBufferToFile(const uint8_t* data,
const AMediaCodecBufferInfo& info) {
if (output_file_.is_open()) {
output_file_.write(reinterpret_cast<const char*>(data), info.size);
if (output_file_.fail()) {
printf("[ERR] Failed to write encoded buffer into file.\n");
}
}
}
void AccumulateOutputBufferSize(const uint8_t* /* data */,
const AMediaCodecBufferInfo& info) {
total_output_buffer_size_ += info.size;
}
protected:
void SetUp() override {
encoder_ = MediaCodecEncoder::Create(g_env->input_file_path(),
g_env->visible_size());
ASSERT_TRUE(encoder_);
encoder_->Rewind();
ASSERT_TRUE(encoder_->Configure(
static_cast<int32_t>(g_env->requested_bitrate()),
static_cast<int32_t>(g_env->requested_framerate())));
ASSERT_TRUE(encoder_->Start());
}
void TearDown() override {
EXPECT_TRUE(encoder_->Stop());
output_file_.close();
encoder_.reset();
}
bool CreateOutputFile() {
if (g_env->output_file_path().empty())
return false;
output_file_.open(g_env->output_file_path(), std::ofstream::binary);
if (!output_file_.is_open()) {
printf("[ERR] Failed to open file: %s\n",
g_env->output_file_path().c_str());
return false;
}
return true;
}
double CalculateAverageBitrate(size_t num_frames,
unsigned int framerate) const {
return 1.0f * total_output_buffer_size_ * 8 / num_frames * framerate;
}
// The wrapper of the mediacodec encoder.
std::unique_ptr<MediaCodecEncoder> encoder_;
// The output file to write the encoded video bitstream.
std::ofstream output_file_;
// Used to accumulate the output buffer size.
size_t total_output_buffer_size_;
};
class LatencyRecorder {
public:
void OnEncodeInputBuffer(uint64_t time_us) {
auto res = start_times_.insert(std::make_pair(time_us, GetNowUs()));
ASSERT_TRUE(res.second);
}
void OnOutputBufferReady(const uint8_t* /* data */,
const AMediaCodecBufferInfo& info) {
// Ignore the CSD buffer and the empty EOS buffer.
if (!(info.flags & AMEDIACODEC_BUFFER_FLAG_CODEC_CONFIG) && info.size != 0)
end_times_[info.presentationTimeUs] = GetNowUs();
}
void PrintResult() const {
std::vector<int64_t> latency_times;
for (auto const& start_kv : start_times_) {
auto end_it = end_times_.find(start_kv.first);
ASSERT_TRUE(end_it != end_times_.end());
latency_times.push_back(end_it->second - start_kv.second);
}
std::sort(latency_times.begin(), latency_times.end());
for (int percentile : kLoggedLatencyPercentiles) {
size_t index = static_cast<size_t>(
std::ceil(0.01f * percentile * latency_times.size())) -
1;
printf("Encode latency for the %dth percentile: %" PRId64 " us\n",
percentile, latency_times[index]);
}
}
private:
// Measured time of enqueueing input buffers and dequeueing output buffers.
// The key is the timestamp of the frame.
std::map<uint64_t, int64_t> start_times_;
std::map<uint64_t, int64_t> end_times_;
};
TEST_F(ArcVideoEncoderE2ETest, TestSimpleEncode) {
// Write the output buffers to file.
if (CreateOutputFile()) {
encoder_->SetOutputBufferReadyCb(
std::bind(&ArcVideoEncoderE2ETest::WriteOutputBufferToFile, this,
std::placeholders::_1, std::placeholders::_2));
}
encoder_->set_run_at_fps(g_env->run_at_fps());
if (g_env->num_encoded_frames())
encoder_->set_num_encoded_frames(g_env->num_encoded_frames());
EXPECT_TRUE(encoder_->Encode());
}
TEST_F(ArcVideoEncoderE2ETest, TestBitrate) {
// Ensure the number of encoded frames is enough for bitrate test case.
encoder_->set_num_encoded_frames(
std::max(encoder_->num_encoded_frames(), kMinNumEncodedFrames));
// Accumulate the size of the output buffers.
total_output_buffer_size_ = 0;
encoder_->SetOutputBufferReadyCb(
std::bind(&ArcVideoEncoderE2ETest::AccumulateOutputBufferSize, this,
std::placeholders::_1, std::placeholders::_2));
// TODO(akahuang): Verify bitrate switch at the middle of stream.
EXPECT_TRUE(encoder_->Encode());
double measured_bitrate = CalculateAverageBitrate(
encoder_->num_encoded_frames(), g_env->requested_framerate());
EXPECT_NEAR(measured_bitrate, g_env->requested_bitrate(),
kBitrateTolerance * g_env->requested_bitrate());
}
TEST_F(ArcVideoEncoderE2ETest, PerfFPS) {
FPSCalculator fps_calculator;
auto callback = [&fps_calculator](const uint8_t* /* data */,
const AMediaCodecBufferInfo& /* info */) {
ASSERT_TRUE(fps_calculator.RecordFrameTimeDiff());
};
// Record frame time differences of the output buffers.
encoder_->SetOutputBufferReadyCb(callback);
EXPECT_TRUE(encoder_->Encode());
double measured_fps = fps_calculator.CalculateFPS();
printf("Measured encoder FPS: %.4f\n", measured_fps);
}
TEST_F(ArcVideoEncoderE2ETest, PerfLatency) {
LatencyRecorder recorder;
encoder_->SetEncodeInputBufferCb(std::bind(
&LatencyRecorder::OnEncodeInputBuffer, &recorder, std::placeholders::_1));
encoder_->SetOutputBufferReadyCb(
std::bind(&LatencyRecorder::OnOutputBufferReady, &recorder,
std::placeholders::_1, std::placeholders::_2));
encoder_->set_run_at_fps(true);
EXPECT_TRUE(encoder_->Encode());
recorder.PrintResult();
}
} // namespace android
bool GetOption(int argc, char** argv, android::CmdlineArgs* args) {
const char* const optstring = "t:rn:";
static const struct option opts[] = {
{"test_stream_data", required_argument, nullptr, 't'},
{"run_at_fps", no_argument, nullptr, 'r'},
{"num_encoded_frames", required_argument, nullptr, 'n'},
{nullptr, 0, nullptr, 0},
};
int opt;
while ((opt = getopt_long(argc, argv, optstring, opts, nullptr)) != -1) {
switch (opt) {
case 't':
args->test_stream_data = optarg;
break;
case 'r':
args->run_at_fps = true;
break;
case 'n':
args->num_encoded_frames = static_cast<size_t>(atoi(optarg));
break;
default:
printf("[WARN] Unknown option: getopt_long() returned code 0x%x.\n",
opt);
break;
}
}
if (args->test_stream_data.empty()) {
printf("[ERR] Please assign test stream data by --test_stream_data\n");
return false;
}
return true;
}
int main(int argc, char** argv) {
android::CmdlineArgs args;
if (!GetOption(argc, argv, &args))
return EXIT_FAILURE;
android::g_env = reinterpret_cast<android::ArcVideoEncoderTestEnvironment*>(
testing::AddGlobalTestEnvironment(
new android::ArcVideoEncoderTestEnvironment(args)));
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}