summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--chromecast/media/BUILD.gn28
-rw-r--r--chromecast/media/cma/backend/multizone_backend_unittest.cc322
-rw-r--r--chromecast/media/media.gyp27
3 files changed, 377 insertions, 0 deletions
diff --git a/chromecast/media/BUILD.gn b/chromecast/media/BUILD.gn
index 02e14e8..99e39e6 100644
--- a/chromecast/media/BUILD.gn
+++ b/chromecast/media/BUILD.gn
@@ -68,3 +68,31 @@ test("cast_media_unittests") {
deps += [ "//chromecast/media/base:libcast_media_1.0_default_core" ]
}
}
+
+test("cast_multizone_backend_unittests") {
+ sources = [
+ "cma/backend/multizone_backend_unittest.cc",
+ "cma/test/run_all_unittests.cc",
+ ]
+
+ deps = [
+ "//base",
+ "//base/test:test_support",
+ "//chromecast/base",
+ "//chromecast/base/metrics:test_support",
+ "//chromecast/media/base:libcast_media_1.0",
+ "//chromecast/media/cma/backend",
+ "//chromecast/media/cma/base",
+ "//chromecast/public",
+ "//media",
+ "//media/base:test_support",
+ "//testing/gmock",
+ "//testing/gtest",
+ ]
+
+ if (chromecast_branding == "public") {
+ # Link default libcast_media_1.0 statically not to link dummy one
+ # dynamically for public unittests.
+ deps += [ "//chromecast/media/base:libcast_media_1.0_default_core" ]
+ }
+}
diff --git a/chromecast/media/cma/backend/multizone_backend_unittest.cc b/chromecast/media/cma/backend/multizone_backend_unittest.cc
new file mode 100644
index 0000000..e080a22
--- /dev/null
+++ b/chromecast/media/cma/backend/multizone_backend_unittest.cc
@@ -0,0 +1,322 @@
+// Copyright 2016 The Chromium 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 <stdint.h>
+#include <stdlib.h>
+
+#include <limits>
+#include <vector>
+
+#include "base/bind.h"
+#include "base/command_line.h"
+#include "base/logging.h"
+#include "base/memory/ref_counted.h"
+#include "base/memory/scoped_ptr.h"
+#include "base/message_loop/message_loop.h"
+#include "base/thread_task_runner_handle.h"
+#include "base/threading/thread_checker.h"
+#include "base/time/time.h"
+#include "chromecast/base/task_runner_impl.h"
+#include "chromecast/media/cma/base/decoder_buffer_adapter.h"
+#include "chromecast/media/cma/base/decoder_config_adapter.h"
+#include "chromecast/public/cast_media_shlib.h"
+#include "chromecast/public/media/cast_decoder_buffer.h"
+#include "chromecast/public/media/decoder_config.h"
+#include "chromecast/public/media/media_pipeline_backend.h"
+#include "chromecast/public/media/media_pipeline_device_params.h"
+#include "media/base/audio_decoder_config.h"
+#include "media/base/decoder_buffer.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace chromecast {
+namespace media {
+
+class MultizoneBackendTest;
+
+namespace {
+
+const int64_t kMicrosecondsPerSecond = 1000 * 1000;
+// Total length of test, in microseconds.
+const int64_t kPushTimeUs = 2 * kMicrosecondsPerSecond;
+const int64_t kStartPts = 0;
+const int64_t kRenderingDelayGracePeriodUs = 250 * 1000;
+const int64_t kMaxRenderingDelayErrorUs = 200;
+const int kNumEffectsStreams = 3;
+
+void IgnoreEos() {}
+
+class BufferFeeder : public MediaPipelineBackend::Decoder::Delegate {
+ public:
+ BufferFeeder(const AudioConfig& config,
+ bool effects_only,
+ const base::Closure& eos_cb);
+ ~BufferFeeder() override {}
+
+ void Initialize();
+ void Start();
+ void Stop();
+
+ private:
+ void FeedBuffer();
+
+ // MediaPipelineBackend::Decoder::Delegate implementation:
+ void OnPushBufferComplete(MediaPipelineBackend::BufferStatus status) override;
+ void OnEndOfStream() override;
+ void OnDecoderError() override {
+ DCHECK(thread_checker_.CalledOnValidThread());
+ if (effects_only_) {
+ feeding_completed_ = true;
+ } else {
+ ASSERT_TRUE(false);
+ }
+ }
+ void OnKeyStatusChanged(const std::string& key_id,
+ CastKeyStatus key_status,
+ uint32_t system_code) override {
+ DCHECK(thread_checker_.CalledOnValidThread());
+ ASSERT_TRUE(false);
+ }
+ void OnVideoResolutionChanged(const Size& size) override {
+ DCHECK(thread_checker_.CalledOnValidThread());
+ }
+
+ const AudioConfig config_;
+ const bool effects_only_;
+ const base::Closure eos_cb_;
+ bool feeding_completed_;
+ scoped_ptr<TaskRunnerImpl> task_runner_;
+ scoped_ptr<MediaPipelineBackend> backend_;
+ MediaPipelineBackend::AudioDecoder* decoder_;
+ int64_t push_limit_us_;
+ int64_t last_push_length_us_;
+ int64_t pushed_us_;
+ int64_t next_push_playback_timestamp_;
+ scoped_refptr<DecoderBufferBase> pending_buffer_;
+ base::ThreadChecker thread_checker_;
+
+ DISALLOW_COPY_AND_ASSIGN(BufferFeeder);
+};
+
+} // namespace
+
+class MultizoneBackendTest : public testing::TestWithParam<int> {
+ public:
+ MultizoneBackendTest();
+ ~MultizoneBackendTest() override;
+
+ void SetUp() override {
+ srand(12345);
+ CastMediaShlib::Initialize(base::CommandLine::ForCurrentProcess()->argv());
+ }
+
+ void TearDown() override {
+ // Pipeline must be destroyed before finalizing media shlib.
+ audio_feeder_.reset();
+ effects_feeders_.clear();
+ CastMediaShlib::Finalize();
+ }
+
+ void AddEffectsStreams();
+
+ void Initialize(int sample_rate);
+ void Start();
+ void OnEndOfStream();
+
+ private:
+ std::vector<scoped_ptr<BufferFeeder>> effects_feeders_;
+ scoped_ptr<BufferFeeder> audio_feeder_;
+
+ DISALLOW_COPY_AND_ASSIGN(MultizoneBackendTest);
+};
+
+namespace {
+
+BufferFeeder::BufferFeeder(const AudioConfig& config,
+ bool effects_only,
+ const base::Closure& eos_cb)
+ : config_(config),
+ effects_only_(effects_only),
+ eos_cb_(eos_cb),
+ feeding_completed_(false),
+ task_runner_(new TaskRunnerImpl()),
+ decoder_(nullptr),
+ push_limit_us_(effects_only_ ? 0 : kPushTimeUs),
+ last_push_length_us_(0),
+ pushed_us_(0),
+ next_push_playback_timestamp_(0) {
+ CHECK(!eos_cb_.is_null());
+}
+
+void BufferFeeder::Initialize() {
+ MediaPipelineDeviceParams params(
+ MediaPipelineDeviceParams::kModeIgnorePts,
+ effects_only_ ? MediaPipelineDeviceParams::kAudioStreamSoundEffects
+ : MediaPipelineDeviceParams::kAudioStreamNormal,
+ task_runner_.get());
+ backend_.reset(CastMediaShlib::CreateMediaPipelineBackend(params));
+ CHECK(backend_);
+
+ decoder_ = backend_->CreateAudioDecoder();
+ CHECK(decoder_);
+ ASSERT_TRUE(decoder_->SetConfig(config_));
+ decoder_->SetDelegate(this);
+
+ ASSERT_TRUE(backend_->Initialize());
+}
+
+void BufferFeeder::Start() {
+ ASSERT_TRUE(backend_->Start(kStartPts));
+ base::ThreadTaskRunnerHandle::Get()->PostTask(
+ FROM_HERE, base::Bind(&BufferFeeder::FeedBuffer, base::Unretained(this)));
+}
+
+void BufferFeeder::Stop() {
+ feeding_completed_ = true;
+ ASSERT_TRUE(backend_->Stop());
+}
+
+void BufferFeeder::FeedBuffer() {
+ CHECK(decoder_);
+ if (feeding_completed_)
+ return;
+
+ if (!effects_only_ && pushed_us_ >= push_limit_us_) {
+ pending_buffer_ = new media::DecoderBufferAdapter(
+ ::media::DecoderBuffer::CreateEOSBuffer());
+ feeding_completed_ = true;
+ last_push_length_us_ = 0;
+ } else {
+ int size_bytes = (rand() % 128 + 16) * 16;
+ int num_samples =
+ size_bytes / (config_.bytes_per_channel * config_.channel_number);
+ last_push_length_us_ =
+ num_samples * kMicrosecondsPerSecond / config_.samples_per_second;
+ scoped_refptr<::media::DecoderBuffer> silence_buffer(
+ new ::media::DecoderBuffer(size_bytes));
+ memset(silence_buffer->writable_data(), 0, silence_buffer->data_size());
+ pending_buffer_ = new media::DecoderBufferAdapter(silence_buffer);
+ pending_buffer_->set_timestamp(
+ base::TimeDelta::FromMicroseconds(pushed_us_));
+ }
+ BufferStatus status = decoder_->PushBuffer(pending_buffer_.get());
+ ASSERT_NE(status, MediaPipelineBackend::kBufferFailed);
+ if (status == MediaPipelineBackend::kBufferPending)
+ return;
+ OnPushBufferComplete(MediaPipelineBackend::kBufferSuccess);
+}
+
+void BufferFeeder::OnEndOfStream() {
+ DCHECK(thread_checker_.CalledOnValidThread());
+ eos_cb_.Run();
+}
+
+void BufferFeeder::OnPushBufferComplete(BufferStatus status) {
+ DCHECK(thread_checker_.CalledOnValidThread());
+ pending_buffer_ = nullptr;
+ ASSERT_NE(status, MediaPipelineBackend::kBufferFailed);
+
+ if (!effects_only_) {
+ MediaPipelineBackend::AudioDecoder::RenderingDelay delay =
+ decoder_->GetRenderingDelay();
+ int64_t expected_next_push_playback_timestamp =
+ next_push_playback_timestamp_ + last_push_length_us_;
+ next_push_playback_timestamp_ =
+ delay.timestamp_microseconds + delay.delay_microseconds;
+ // Check rendering delay accuracy only if we have pushed more than
+ // kRenderingDelayGracePeriodUs of data.
+ if (pushed_us_ > kRenderingDelayGracePeriodUs) {
+ int64_t error =
+ next_push_playback_timestamp_ - expected_next_push_playback_timestamp;
+ EXPECT_LT(std::abs(error), kMaxRenderingDelayErrorUs)
+ << "Bad rendering delay after " << pushed_us_ << " us";
+ }
+ }
+ pushed_us_ += last_push_length_us_;
+
+ if (feeding_completed_)
+ return;
+
+ base::ThreadTaskRunnerHandle::Get()->PostTask(
+ FROM_HERE, base::Bind(&BufferFeeder::FeedBuffer, base::Unretained(this)));
+}
+
+} // namespace
+
+MultizoneBackendTest::MultizoneBackendTest() {}
+
+MultizoneBackendTest::~MultizoneBackendTest() {}
+
+void MultizoneBackendTest::Initialize(int sample_rate) {
+ AudioConfig config;
+ config.codec = kCodecPCM;
+ config.sample_format = kSampleFormatPlanarF32;
+ config.channel_number = 2;
+ config.bytes_per_channel = 4;
+ config.samples_per_second = sample_rate;
+
+ audio_feeder_.reset(
+ new BufferFeeder(config, false /* effects_only */,
+ base::Bind(&MultizoneBackendTest::OnEndOfStream,
+ base::Unretained(this))));
+ audio_feeder_->Initialize();
+}
+
+void MultizoneBackendTest::AddEffectsStreams() {
+ AudioConfig effects_config;
+ effects_config.codec = kCodecPCM;
+ effects_config.sample_format = kSampleFormatS16;
+ effects_config.channel_number = 2;
+ effects_config.bytes_per_channel = 2;
+ effects_config.samples_per_second = 48000;
+
+ for (int i = 0; i < kNumEffectsStreams; ++i) {
+ scoped_ptr<BufferFeeder> feeder(new BufferFeeder(
+ effects_config, true /* effects_only */, base::Bind(&IgnoreEos)));
+ feeder->Initialize();
+ effects_feeders_.push_back(std::move(feeder));
+ }
+}
+
+void MultizoneBackendTest::Start() {
+ for (auto& feeder : effects_feeders_)
+ feeder->Start();
+ CHECK(audio_feeder_);
+ audio_feeder_->Start();
+}
+
+void MultizoneBackendTest::OnEndOfStream() {
+ audio_feeder_->Stop();
+ for (auto& feeder : effects_feeders_)
+ feeder->Stop();
+
+ base::MessageLoop::current()->QuitWhenIdle();
+}
+
+TEST_P(MultizoneBackendTest, RenderingDelay) {
+ scoped_ptr<base::MessageLoop> message_loop(new base::MessageLoop());
+
+ Initialize(GetParam());
+ AddEffectsStreams();
+ Start();
+ message_loop->Run();
+}
+
+INSTANTIATE_TEST_CASE_P(Required,
+ MultizoneBackendTest,
+ ::testing::Values(8000,
+ 11025,
+ 12000,
+ 16000,
+ 22050,
+ 24000,
+ 32000,
+ 44100,
+ 48000));
+
+INSTANTIATE_TEST_CASE_P(Optional,
+ MultizoneBackendTest,
+ ::testing::Values(64000, 88200, 96000));
+
+} // namespace media
+} // namespace chromecast
diff --git a/chromecast/media/media.gyp b/chromecast/media/media.gyp
index 359e9ae..1430fe5 100644
--- a/chromecast/media/media.gyp
+++ b/chromecast/media/media.gyp
@@ -313,6 +313,33 @@
}],
],
},
+ {
+ 'target_name': 'cast_multizone_backend_unittests',
+ 'type': '<(gtest_target_type)',
+ 'dependencies': [
+ 'cast_media',
+ '../../base/base.gyp:base',
+ '../../base/base.gyp:test_support_base',
+ '../../chromecast/chromecast.gyp:cast_metrics_test_support',
+ '../../media/media.gyp:media_test_support',
+ '../../testing/gmock.gyp:gmock',
+ '../../testing/gtest.gyp:gtest',
+ '../../testing/gtest.gyp:gtest_main',
+ ],
+ 'sources': [
+ 'cma/backend/multizone_backend_unittest.cc',
+ 'cma/test/run_all_unittests.cc',
+ ],
+ 'conditions': [
+ ['chromecast_branding=="public"', {
+ 'dependencies': [
+ # Link default libcast_media_1.0 statically not to link dummy one
+ # dynamically for public unittests.
+ 'libcast_media_1.0_default_core',
+ ],
+ }],
+ ],
+ },
{ # Target for OEM partners to override media shared library, i.e.
# libcast_media_1.0.so. This target is only used to build executables
# with correct linkage information.