| // 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(); |
| } |