From b9180fc0499a7c1480a47e17328e7a35bb788086 Mon Sep 17 00:00:00 2001 From: John Koleszar Date: Wed, 16 May 2012 15:27:00 -0700 Subject: [PATCH] Add initial keyframe tests Implements a couple simple tests of the encoder API using the gtest framework: TestDisableKeyframes TestForceKeyframe TestKeyframeMaxDistance Change-Id: I38e93fe242fbeb30bb11b23ac12de8ddc291a28d --- test/encode_test_driver.cc | 137 ++++++++++++++++++++++++++++ test/encode_test_driver.h | 178 +++++++++++++++++++++++++++++++++++++ test/keyframe_test.cc | 103 +++++++++++++++++++++ test/test.mk | 4 + test/video_source.h | 109 +++++++++++++++++++++++ 5 files changed, 531 insertions(+) create mode 100644 test/encode_test_driver.cc create mode 100644 test/encode_test_driver.h create mode 100644 test/keyframe_test.cc create mode 100644 test/video_source.h diff --git a/test/encode_test_driver.cc b/test/encode_test_driver.cc new file mode 100644 index 000000000..0a188facf --- /dev/null +++ b/test/encode_test_driver.cc @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2012 The WebM project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ +#include "test/encode_test_driver.h" +#include "test/video_source.h" +#include "third_party/googletest/src/include/gtest/gtest.h" + +namespace libvpx_test { + +void Encoder::EncodeFrame(VideoSource *video, unsigned long flags) { + if (video->img()) + EncodeFrameInternal(*video, flags); + else + Flush(); + + // Handle twopass stats + CxDataIterator iter = GetCxData(); + + while (const vpx_codec_cx_pkt_t *pkt = iter.Next()) { + if (pkt->kind != VPX_CODEC_STATS_PKT) + continue; + + stats_->Append(*pkt); + } +} + +void Encoder::EncodeFrameInternal(const VideoSource &video, + unsigned long flags) { + vpx_codec_err_t res; + const vpx_image_t *img = video.img(); + + // Handle first frame initialization + if (!encoder_.priv) { + cfg_.g_w = img->d_w; + cfg_.g_h = img->d_h; + cfg_.g_timebase = video.timebase(); + cfg_.rc_twopass_stats_in = stats_->buf(); + res = vpx_codec_enc_init(&encoder_, &vpx_codec_vp8_cx_algo, &cfg_, 0); + ASSERT_EQ(VPX_CODEC_OK, res) << EncoderError(); + } + + // Handle frame resizing + if (cfg_.g_w != img->d_w || cfg_.g_h != img->d_h) { + cfg_.g_w = img->d_w; + cfg_.g_h = img->d_h; + res = vpx_codec_enc_config_set(&encoder_, &cfg_); + ASSERT_EQ(VPX_CODEC_OK, res) << EncoderError(); + } + + // Encode the frame + res = vpx_codec_encode(&encoder_, + video.img(), video.pts(), video.duration(), + flags, deadline_); + ASSERT_EQ(VPX_CODEC_OK, res) << EncoderError(); +} + +void Encoder::Flush() { + const vpx_codec_err_t res = vpx_codec_encode(&encoder_, NULL, 0, 0, 0, + deadline_); + ASSERT_EQ(VPX_CODEC_OK, res) << EncoderError(); +} + +void EncoderTest::SetMode(TestMode mode) { + switch (mode) { + case kRealTime: + deadline_ = VPX_DL_REALTIME; + break; + + case kOnePassGood: + case kTwoPassGood: + deadline_ = VPX_DL_GOOD_QUALITY; + break; + + case kOnePassBest: + case kTwoPassBest: + deadline_ = VPX_DL_BEST_QUALITY; + break; + + default: + ASSERT_TRUE(false) << "Unexpected mode " << mode; + } + + if (mode == kTwoPassGood || mode == kTwoPassBest) + passes_ = 2; + else + passes_ = 1; +} + +void EncoderTest::RunLoop(VideoSource *video) { + for (unsigned int pass = 0; pass < passes_; pass++) { + if (passes_ == 1) + cfg_.g_pass = VPX_RC_ONE_PASS; + else if (pass == 0) + cfg_.g_pass = VPX_RC_FIRST_PASS; + else + cfg_.g_pass = VPX_RC_LAST_PASS; + + BeginPassHook(pass); + Encoder encoder(cfg_, deadline_, &stats_); + + bool again; + + for (video->Begin(), again = true; again; video->Next()) { + again = video->img() != NULL; + + PreEncodeFrameHook(video); + encoder.EncodeFrame(video, flags_); + + CxDataIterator iter = encoder.GetCxData(); + + while (const vpx_codec_cx_pkt_t *pkt = iter.Next()) { + again = true; + + if (pkt->kind != VPX_CODEC_CX_FRAME_PKT) + continue; + + FramePktHook(pkt); + } + + if (!Continue()) + break; + } + + EndPassHook(); + + if (!Continue()) + break; + } +} + +} // namespace libvpx_test diff --git a/test/encode_test_driver.h b/test/encode_test_driver.h new file mode 100644 index 000000000..43faaaed7 --- /dev/null +++ b/test/encode_test_driver.h @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2012 The WebM project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ +#ifndef TEST_ENCODE_TEST_DRIVER_H_ +#define TEST_ENCODE_TEST_DRIVER_H_ +#include +#include +#include "third_party/googletest/src/include/gtest/gtest.h" +#include "vpx/vpx_encoder.h" +#include "vpx/vp8cx.h" + +namespace libvpx_test { + +class VideoSource; + +enum TestMode { + kRealTime, + kOnePassGood, + kOnePassBest, + kTwoPassGood, + kTwoPassBest +}; +#define ALL_TEST_MODES ::testing::Values(::libvpx_test::kRealTime, \ + ::libvpx_test::kOnePassGood, \ + ::libvpx_test::kOnePassBest, \ + ::libvpx_test::kTwoPassGood, \ + ::libvpx_test::kTwoPassBest) + + +// Provides an object to handle the libvpx get_cx_data() iteration pattern +class CxDataIterator { + public: + explicit CxDataIterator(vpx_codec_ctx_t *encoder) + : encoder_(encoder), iter_(NULL) {} + + const vpx_codec_cx_pkt_t *Next() { + return vpx_codec_get_cx_data(encoder_, &iter_); + } + + private: + vpx_codec_ctx_t *encoder_; + vpx_codec_iter_t iter_; +}; + + +// Implements an in-memory store for libvpx twopass statistics +class TwopassStatsStore { + public: + void Append(const vpx_codec_cx_pkt_t &pkt) { + buffer_.append(reinterpret_cast(pkt.data.twopass_stats.buf), + pkt.data.twopass_stats.sz); + } + + vpx_fixed_buf_t buf() { + const vpx_fixed_buf_t buf = { &buffer_[0], buffer_.size() }; + return buf; + } + + protected: + std::string buffer_; +}; + + +// Provides a simplified interface to manage one video encoding pass, given +// a configuration and video source. +// +// TODO(jkoleszar): The exact services it provides and the appropriate +// level of abstraction will be fleshed out as more tests are written. +class Encoder { + public: + Encoder(vpx_codec_enc_cfg_t cfg, unsigned long deadline, + TwopassStatsStore *stats) + : cfg_(cfg), deadline_(deadline), stats_(stats) { + memset(&encoder_, 0, sizeof(encoder_)); + } + + ~Encoder() { + vpx_codec_destroy(&encoder_); + } + + CxDataIterator GetCxData() { + return CxDataIterator(&encoder_); + } + + // This is a thin wrapper around vpx_codec_encode(), so refer to + // vpx_encoder.h for its semantics. + void EncodeFrame(VideoSource *video, unsigned long flags); + + // Convenience wrapper for EncodeFrame() + void EncodeFrame(VideoSource *video) { + EncodeFrame(video, 0); + } + + void set_deadline(unsigned long deadline) { + deadline_ = deadline; + } + + protected: + const char *EncoderError() { + const char *detail = vpx_codec_error_detail(&encoder_); + return detail ? detail : vpx_codec_error(&encoder_); + } + + // Encode an image + void EncodeFrameInternal(const VideoSource &video, unsigned long flags); + + // Flush the encoder on EOS + void Flush(); + + vpx_codec_ctx_t encoder_; + vpx_codec_enc_cfg_t cfg_; + unsigned long deadline_; + TwopassStatsStore *stats_; +}; + + +// Common test functionality for all Encoder tests. +// +// This class is a mixin which provides the main loop common to all +// encoder tests. It provides hooks which can be overridden by subclasses +// to implement each test's specific behavior, while centralizing the bulk +// of the boilerplate. Note that it doesn't inherit the gtest testing +// classes directly, so that tests can be parameterized differently. +class EncoderTest { + protected: + EncoderTest() : abort_(false), flags_(0) {} + + virtual ~EncoderTest() {} + + // Initialize the cfg_ member with the default configuration. + void InitializeConfig() { + const vpx_codec_err_t res = vpx_codec_enc_config_default( + &vpx_codec_vp8_cx_algo, &cfg_, 0); + ASSERT_EQ(VPX_CODEC_OK, res); + } + + // Map the TestMode enum to the deadline_ and passes_ variables. + void SetMode(TestMode mode); + + // Main loop. + virtual void RunLoop(VideoSource *video); + + // Hook to be called at the beginning of a pass. + virtual void BeginPassHook(unsigned int pass) {} + + // Hook to be called at the end of a pass. + virtual void EndPassHook() {} + + // Hook to be called before encoding a frame. + virtual void PreEncodeFrameHook(VideoSource *video) {} + + // Hook to be called on every compressed data packet. + virtual void FramePktHook(const vpx_codec_cx_pkt_t *pkt) {} + + // Hook to determine whether the encode loop should continue. + virtual bool Continue() const { return !abort_; } + + bool abort_; + vpx_codec_enc_cfg_t cfg_; + unsigned int passes_; + unsigned long deadline_; + TwopassStatsStore stats_; + unsigned long flags_; +}; + +} // namespace libvpx_test + +// Macros to be used with ::testing::Combine +#define PARAMS(...) ::testing::TestWithParam< std::tr1::tuple< __VA_ARGS__ > > +#define GET_PARAM(k) std::tr1::get< k >(GetParam()) + +#endif // TEST_ENCODE_TEST_DRIVER_H_ diff --git a/test/keyframe_test.cc b/test/keyframe_test.cc new file mode 100644 index 000000000..db18e5dab --- /dev/null +++ b/test/keyframe_test.cc @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2012 The WebM project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ +#include +#include +#include "test/encode_test_driver.h" +#include "test/video_source.h" +#include "third_party/googletest/src/include/gtest/gtest.h" + +namespace { + +class KeyframeTest : public ::libvpx_test::EncoderTest, + public ::testing::TestWithParam { + protected: + virtual void SetUp() { + InitializeConfig(); + SetMode(GetParam()); + kf_count_ = 0; + kf_count_max_ = INT_MAX; + kf_do_force_kf_ = false; + } + + virtual bool Continue() { + return !HasFatalFailure() && !abort_; + } + + virtual void PreEncodeFrameHook(::libvpx_test::VideoSource *video) { + if (kf_do_force_kf_) + flags_ = (video->frame() % 3) ? 0 : VPX_EFLAG_FORCE_KF; + } + + virtual void FramePktHook(const vpx_codec_cx_pkt_t *pkt) { + if (pkt->data.frame.flags & VPX_FRAME_IS_KEY) { + kf_pts_list_.push_back(pkt->data.frame.pts); + kf_count_++; + abort_ |= kf_count_ > kf_count_max_; + } + } + + bool kf_do_force_kf_; + int kf_count_; + int kf_count_max_; + std::vector< vpx_codec_pts_t > kf_pts_list_; +}; + +TEST_P(KeyframeTest, TestRandomVideoSource) { + // Validate that encoding the RandomVideoSource produces multiple keyframes. + // This validates the results of the TestDisableKeyframes test. + kf_count_max_ = 2; // early exit successful tests. + + ::libvpx_test::RandomVideoSource video; + ASSERT_NO_FATAL_FAILURE(RunLoop(&video)); + + EXPECT_GT(kf_count_, 1); +} + +TEST_P(KeyframeTest, TestDisableKeyframes) { + cfg_.kf_mode = VPX_KF_DISABLED; + kf_count_max_ = 1; // early exit failed tests. + + ::libvpx_test::RandomVideoSource video; + ASSERT_NO_FATAL_FAILURE(RunLoop(&video)); + + EXPECT_EQ(1, kf_count_); +} + +TEST_P(KeyframeTest, TestForceKeyframe) { + cfg_.kf_mode = VPX_KF_DISABLED; + kf_do_force_kf_ = true; + + ::libvpx_test::DummyVideoSource video; + ASSERT_NO_FATAL_FAILURE(RunLoop(&video)); + + // verify that every third frame is a keyframe. + for (std::vector::iterator iter = kf_pts_list_.begin(); + iter != kf_pts_list_.end(); + ++iter) { + ASSERT_EQ(0, *iter % 3) << "Unexpected keyframe at frame " << *iter; + } +} + +TEST_P(KeyframeTest, TestKeyframeMaxDistance) { + cfg_.kf_max_dist = 25; + + ::libvpx_test::DummyVideoSource video; + ASSERT_NO_FATAL_FAILURE(RunLoop(&video)); + + // verify that keyframe interval matches kf_max_dist + for (std::vector::iterator iter = kf_pts_list_.begin(); + iter != kf_pts_list_.end(); + iter++) { + ASSERT_EQ(0, *iter % 25) << "Unexpected keyframe at frame " << *iter; + } +} + +INSTANTIATE_TEST_CASE_P(AllModes, KeyframeTest, ALL_TEST_MODES); +} // namespace diff --git a/test/test.mk b/test/test.mk index 51382ee96..d04ad170f 100644 --- a/test/test.mk +++ b/test/test.mk @@ -1,4 +1,8 @@ LIBVPX_TEST_SRCS-yes += test.mk LIBVPX_TEST_SRCS-yes += boolcoder_test.cc +LIBVPX_TEST_SRCS-yes += encode_test_driver.cc +LIBVPX_TEST_SRCS-yes += encode_test_driver.h LIBVPX_TEST_SRCS-yes += idctllm_test.cc +LIBVPX_TEST_SRCS-yes += keyframe_test.cc LIBVPX_TEST_SRCS-yes += test_libvpx.cc +LIBVPX_TEST_SRCS-yes += video_source.h diff --git a/test/video_source.h b/test/video_source.h new file mode 100644 index 000000000..86c6caa83 --- /dev/null +++ b/test/video_source.h @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2012 The WebM project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ +#ifndef TEST_VIDEO_SOURCE_H_ +#define TEST_VIDEO_SOURCE_H_ +#include "vpx/vpx_encoder.h" + +namespace libvpx_test { + +// Abstract base class for test video sources, which provide a stream of +// vpx_image_t images with associated timestamps and duration. +class VideoSource { + public: + virtual ~VideoSource() {} + + // Prepare the stream for reading, rewind/open as necessary. + virtual void Begin() = 0; + + // Advance the cursor to the next frame + virtual void Next() = 0; + + // Get the current video frame, or NULL on End-Of-Stream. + virtual vpx_image_t *img() const = 0; + + // Get the presentation timestamp of the current frame. + virtual vpx_codec_pts_t pts() const = 0; + + // Get the current frame's duration + virtual unsigned long duration() const = 0; + + // Get the timebase for the stream + virtual vpx_rational_t timebase() const = 0; + + // Get the current frame counter, starting at 0. + virtual unsigned int frame() const = 0; +}; + + +class DummyVideoSource : public VideoSource { + public: + DummyVideoSource() + : img_(NULL), limit_(100) { SetSize(80, 64); } + + virtual ~DummyVideoSource() { vpx_img_free(img_); } + + virtual void Begin() { + frame_ = 0; + FillFrame(); + } + + virtual void Next() { + ++frame_; + FillFrame(); + } + + virtual vpx_image_t *img() const { + return (frame_ < limit_) ? img_ : NULL; + } + + // Models a stream where Timebase = 1/FPS, so pts == frame. + virtual vpx_codec_pts_t pts() const { return frame_; } + + virtual unsigned long duration() const { return 1; } + + virtual vpx_rational_t timebase() const { + const vpx_rational_t t = {1, 30}; + return t; + } + + virtual unsigned int frame() const { return frame_; } + + void SetSize(unsigned int width, unsigned int height) { + vpx_img_free(img_); + raw_sz_ = ((width + 31)&~31) * height * 3 / 2; + img_ = vpx_img_alloc(NULL, VPX_IMG_FMT_VPXI420, width, height, 32); + } + + protected: + virtual void FillFrame() { memset(img_->img_data, 0, raw_sz_); } + + vpx_image_t *img_; + size_t raw_sz_; + unsigned int limit_; + unsigned int frame_; +}; + + +class RandomVideoSource : public DummyVideoSource { + protected: + // 15 frames of noise, followed by 15 static frames. Reset to 0 rather + // than holding previous frames to encourage keyframes to be thrown. + virtual void FillFrame() { + if (frame_ % 30 < 15) + for (size_t i = 0; i < raw_sz_; ++i) + img_->img_data[i] = rand(); + else + memset(img_->img_data, 0, raw_sz_); + } +}; + +} // namespace libvpx_test + +#endif // TEST_VIDEO_SOURCE_H_