| // Copyright 2013 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 <stddef.h> |
| #include <stdint.h> |
| |
| #include <algorithm> |
| #include <cmath> |
| #include <memory> |
| #include <vector> |
| |
| #include "base/callback_helpers.h" |
| #include "base/command_line.h" |
| #include "base/macros.h" |
| #include "base/run_loop.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/threading/thread_restrictions.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/extensions/extension_apitest.h" |
| #include "chrome/common/chrome_switches.h" |
| #include "content/public/common/content_features.h" |
| #include "content/public/common/content_switches.h" |
| #include "extensions/common/switches.h" |
| #include "media/base/bind_to_current_loop.h" |
| #include "media/base/video_frame.h" |
| #include "media/cast/cast_config.h" |
| #include "media/cast/cast_environment.h" |
| #include "media/cast/test/utility/audio_utility.h" |
| #include "media/cast/test/utility/default_config.h" |
| #include "media/cast/test/utility/in_process_receiver.h" |
| #include "media/cast/test/utility/net_utility.h" |
| #include "media/cast/test/utility/standalone_cast_environment.h" |
| #include "net/base/net_errors.h" |
| #include "net/base/rand_callback.h" |
| #include "net/log/net_log_source.h" |
| #include "net/socket/udp_server_socket.h" |
| #include "testing/gtest/include/gtest/gtest-param-test.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| using media::cast::test::GetFreeLocalPort; |
| |
| namespace extensions { |
| |
| class CastStreamingApiTest : public ExtensionApiTest { |
| public: |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| ExtensionApiTest::SetUpCommandLine(command_line); |
| command_line->AppendSwitchASCII( |
| extensions::switches::kWhitelistedExtensionID, |
| "ddchlicdkolnonkihahngkmmmjnjlkkf"); |
| command_line->AppendSwitchASCII(::switches::kWindowSize, "300,300"); |
| } |
| }; |
| |
| // Test running the test extension for Cast Mirroring API. |
| IN_PROC_BROWSER_TEST_F(CastStreamingApiTest, Basics) { |
| ASSERT_TRUE(RunExtensionSubtest("cast_streaming", "basics.html")) << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_F(CastStreamingApiTest, Stats) { |
| ASSERT_TRUE(RunExtensionSubtest("cast_streaming", "stats.html")) << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_F(CastStreamingApiTest, BadLogging) { |
| ASSERT_TRUE(RunExtensionSubtest("cast_streaming", "bad_logging.html")) |
| << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_F(CastStreamingApiTest, DestinationNotSet) { |
| ASSERT_TRUE(RunExtensionSubtest("cast_streaming", "destination_not_set.html")) |
| << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_F(CastStreamingApiTest, StopNoStart) { |
| ASSERT_TRUE(RunExtensionSubtest("cast_streaming", "stop_no_start.html")) |
| << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_F(CastStreamingApiTest, NullStream) { |
| ASSERT_TRUE(RunExtensionSubtest("cast_streaming", "null_stream.html")) |
| << message_; |
| } |
| |
| namespace { |
| |
| struct YUVColor { |
| int y; |
| int u; |
| int v; |
| |
| YUVColor() : y(0), u(0), v(0) {} |
| YUVColor(int y_val, int u_val, int v_val) : y(y_val), u(u_val), v(v_val) {} |
| }; |
| |
| media::cast::FrameReceiverConfig WithFakeAesKeyAndIv( |
| media::cast::FrameReceiverConfig config) { |
| config.aes_key = "0123456789abcdef"; |
| config.aes_iv_mask = "fedcba9876543210"; |
| return config; |
| } |
| |
| // An in-process Cast receiver that examines the audio/video frames being |
| // received for expected colors and tones. Used in |
| // CastStreamingApiTest.EndToEnd, below. |
| class TestPatternReceiver : public media::cast::InProcessReceiver { |
| public: |
| explicit TestPatternReceiver( |
| const scoped_refptr<media::cast::CastEnvironment>& cast_environment, |
| const net::IPEndPoint& local_end_point) |
| : InProcessReceiver( |
| cast_environment, |
| local_end_point, |
| net::IPEndPoint(), |
| WithFakeAesKeyAndIv(media::cast::GetDefaultAudioReceiverConfig()), |
| WithFakeAesKeyAndIv(media::cast::GetDefaultVideoReceiverConfig())) { |
| } |
| |
| ~TestPatternReceiver() override {} |
| |
| void AddExpectedTone(int tone_frequency) { |
| expected_tones_.push_back(tone_frequency); |
| } |
| |
| void AddExpectedColor(const YUVColor& yuv_color) { |
| expected_yuv_colors_.push_back(yuv_color); |
| } |
| |
| // Blocks the caller until all expected tones and colors have been observed. |
| void WaitForExpectedTonesAndColors() { |
| base::RunLoop run_loop; |
| cast_env()->PostTask( |
| media::cast::CastEnvironment::MAIN, |
| FROM_HERE, |
| base::Bind(&TestPatternReceiver::NotifyOnceObservedAllTonesAndColors, |
| base::Unretained(this), |
| media::BindToCurrentLoop(run_loop.QuitClosure()))); |
| run_loop.Run(); |
| } |
| |
| private: |
| void NotifyOnceObservedAllTonesAndColors(const base::Closure& done_callback) { |
| DCHECK(cast_env()->CurrentlyOn(media::cast::CastEnvironment::MAIN)); |
| done_callback_ = done_callback; |
| MaybeRunDoneCallback(); |
| } |
| |
| void MaybeRunDoneCallback() { |
| DCHECK(cast_env()->CurrentlyOn(media::cast::CastEnvironment::MAIN)); |
| if (done_callback_.is_null()) |
| return; |
| if (expected_tones_.empty() && expected_yuv_colors_.empty()) { |
| base::ResetAndReturn(&done_callback_).Run(); |
| } else { |
| LOG(INFO) << "Waiting to encounter " << expected_tones_.size() |
| << " more tone(s) and " << expected_yuv_colors_.size() |
| << " more color(s)."; |
| } |
| } |
| |
| // Invoked by InProcessReceiver for each received audio frame. |
| void OnAudioFrame(std::unique_ptr<media::AudioBus> audio_frame, |
| const base::TimeTicks& playout_time, |
| bool is_continuous) override { |
| DCHECK(cast_env()->CurrentlyOn(media::cast::CastEnvironment::MAIN)); |
| |
| if (audio_frame->frames() <= 0) { |
| NOTREACHED() << "OnAudioFrame called with no samples?!?"; |
| return; |
| } |
| |
| if (done_callback_.is_null() || expected_tones_.empty()) |
| return; // No need to waste CPU doing analysis on the signal. |
| |
| // Assume the audio signal is a single sine wave (it can have some |
| // low-amplitude noise). Count zero crossings, and extrapolate the |
| // frequency of the sine wave in |audio_frame|. |
| int crossings = 0; |
| for (int ch = 0; ch < audio_frame->channels(); ++ch) { |
| crossings += media::cast::CountZeroCrossings(audio_frame->channel(ch), |
| audio_frame->frames()); |
| } |
| crossings /= audio_frame->channels(); // Take the average. |
| const float seconds_per_frame = |
| audio_frame->frames() / static_cast<float>(audio_config().rtp_timebase); |
| const float frequency = crossings / seconds_per_frame / 2.0f; |
| VLOG(1) << "Current audio tone frequency: " << frequency; |
| |
| const int kTargetWindowHz = 20; |
| for (auto it = expected_tones_.begin(); it != expected_tones_.end(); ++it) { |
| if (abs(static_cast<int>(frequency) - *it) < kTargetWindowHz) { |
| LOG(INFO) << "Heard tone at frequency " << *it << " Hz."; |
| expected_tones_.erase(it); |
| MaybeRunDoneCallback(); |
| break; |
| } |
| } |
| } |
| |
| void OnVideoFrame(const scoped_refptr<media::VideoFrame>& video_frame, |
| const base::TimeTicks& playout_time, |
| bool is_continuous) override { |
| DCHECK(cast_env()->CurrentlyOn(media::cast::CastEnvironment::MAIN)); |
| |
| CHECK(video_frame->format() == media::PIXEL_FORMAT_YV12 || |
| video_frame->format() == media::PIXEL_FORMAT_I420 || |
| video_frame->format() == media::PIXEL_FORMAT_I420A); |
| |
| if (done_callback_.is_null() || expected_yuv_colors_.empty()) |
| return; // No need to waste CPU doing analysis on the frame. |
| |
| // Take the median value of each plane because the test image will contain a |
| // letterboxed content region of mostly a solid color plus a small piece of |
| // "something" that's animating to keep the tab capture pipeline generating |
| // new frames. |
| const gfx::Rect region = FindLetterboxedContentRegion(video_frame.get()); |
| YUVColor current_color; |
| current_color.y = ComputeMedianIntensityInRegionInPlane( |
| region, |
| video_frame->stride(media::VideoFrame::kYPlane), |
| video_frame->data(media::VideoFrame::kYPlane)); |
| current_color.u = ComputeMedianIntensityInRegionInPlane( |
| gfx::ScaleToEnclosedRect(region, 0.5f), |
| video_frame->stride(media::VideoFrame::kUPlane), |
| video_frame->data(media::VideoFrame::kUPlane)); |
| current_color.v = ComputeMedianIntensityInRegionInPlane( |
| gfx::ScaleToEnclosedRect(region, 0.5f), |
| video_frame->stride(media::VideoFrame::kVPlane), |
| video_frame->data(media::VideoFrame::kVPlane)); |
| VLOG(1) << "Current video color: yuv(" << current_color.y << ", " |
| << current_color.u << ", " << current_color.v << ')'; |
| |
| // Note: The range of acceptable colors is quite large because there's no |
| // way to know whether software compositing is being used for screen |
| // capture; and, if software compositing is being used, there is no color |
| // space management and color values can be off by a lot. That said, color |
| // accuracy is being tested by a suite of content_browsertests. |
| const int kTargetWindow = 50; |
| for (auto it = expected_yuv_colors_.begin(); |
| it != expected_yuv_colors_.end(); ++it) { |
| if (abs(current_color.y - it->y) < kTargetWindow && |
| abs(current_color.u - it->u) < kTargetWindow && |
| abs(current_color.v - it->v) < kTargetWindow) { |
| LOG(INFO) << "Saw expected color yuv(" << it->y << ", " << it->u << ", " |
| << it->v << ") as yuv(" << current_color.y << ", " |
| << current_color.u << ", " << current_color.v << ")."; |
| expected_yuv_colors_.erase(it); |
| MaybeRunDoneCallback(); |
| break; |
| } |
| } |
| } |
| |
| // Return the region that excludes the black letterboxing borders surrounding |
| // the content within |frame|, if any. |
| static gfx::Rect FindLetterboxedContentRegion( |
| const media::VideoFrame* frame) { |
| const int kNonBlackIntensityThreshold = 20; // 16 plus some fuzz. |
| const int width = frame->row_bytes(media::VideoFrame::kYPlane); |
| const int height = frame->rows(media::VideoFrame::kYPlane); |
| const int stride = frame->stride(media::VideoFrame::kYPlane); |
| |
| gfx::Rect result; |
| |
| // Scan from the bottom-right until the first non-black pixel is |
| // encountered. |
| for (int y = height - 1; y >= 0; --y) { |
| const uint8_t* const start = |
| frame->data(media::VideoFrame::kYPlane) + y * stride; |
| const uint8_t* const end = start + width; |
| for (const uint8_t* p = end - 1; p >= start; --p) { |
| if (*p > kNonBlackIntensityThreshold) { |
| result.set_width(p - start + 1); |
| result.set_height(y + 1); |
| y = 0; // Discontinue outer loop. |
| break; |
| } |
| } |
| } |
| |
| // Scan from the upper-left until the first non-black pixel is encountered. |
| for (int y = 0; y < result.height(); ++y) { |
| const uint8_t* const start = |
| frame->data(media::VideoFrame::kYPlane) + y * stride; |
| const uint8_t* const end = start + result.width(); |
| for (const uint8_t* p = start; p < end; ++p) { |
| if (*p > kNonBlackIntensityThreshold) { |
| result.set_x(p - start); |
| result.set_width(result.width() - result.x()); |
| result.set_y(y); |
| result.set_height(result.height() - result.y()); |
| y = result.height(); // Discontinue outer loop. |
| break; |
| } |
| } |
| } |
| |
| return result; |
| } |
| |
| static uint8_t ComputeMedianIntensityInRegionInPlane(const gfx::Rect& region, |
| int stride, |
| const uint8_t* data) { |
| if (region.IsEmpty()) |
| return 0; |
| const size_t num_values = region.size().GetArea(); |
| std::unique_ptr<uint8_t[]> values(new uint8_t[num_values]); |
| for (int y = 0; y < region.height(); ++y) { |
| memcpy(values.get() + y * region.width(), |
| data + (region.y() + y) * stride + region.x(), |
| region.width()); |
| } |
| const size_t middle_idx = num_values / 2; |
| std::nth_element(values.get(), |
| values.get() + middle_idx, |
| values.get() + num_values); |
| return values[middle_idx]; |
| } |
| |
| std::vector<int> expected_tones_; |
| std::vector<YUVColor> expected_yuv_colors_; |
| base::Closure done_callback_; |
| |
| DISALLOW_COPY_AND_ASSIGN(TestPatternReceiver); |
| }; |
| |
| } // namespace |
| |
| class CastStreamingApiTestWithPixelOutput |
| : public CastStreamingApiTest, |
| public testing::WithParamInterface<bool> { |
| public: |
| CastStreamingApiTestWithPixelOutput() { |
| std::vector<base::Feature> audio_service_oop_features = { |
| features::kAudioServiceAudioStreams, |
| features::kAudioServiceOutOfProcess}; |
| if (GetParam()) { |
| // Force audio service out of process to enabled. |
| audio_service_features_.InitWithFeatures(audio_service_oop_features, {}); |
| } else { |
| // Force audio service out of process to disabled. |
| audio_service_features_.InitWithFeatures({}, audio_service_oop_features); |
| } |
| } |
| |
| void SetUp() override { |
| EnablePixelOutput(); |
| CastStreamingApiTest::SetUp(); |
| } |
| |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| command_line->AppendSwitchASCII(::switches::kWindowSize, "128,128"); |
| CastStreamingApiTest::SetUpCommandLine(command_line); |
| } |
| |
| private: |
| base::test::ScopedFeatureList audio_service_features_; |
| }; |
| |
| // Tests the Cast streaming API and its basic functionality end-to-end. An |
| // extension subtest is run to generate test content, capture that content, and |
| // use the API to send it out. At the same time, this test launches an |
| // in-process Cast receiver, listening on a localhost UDP socket, to receive the |
| // content and check whether it matches expectations. |
| #if defined(NDEBUG) && !defined(OS_MACOSX) |
| #define MAYBE_EndToEnd EndToEnd |
| #else |
| // Flaky on Mac: https://crbug.com/841387 |
| #define MAYBE_EndToEnd DISABLED_EndToEnd // crbug.com/396413 |
| #endif |
| IN_PROC_BROWSER_TEST_P(CastStreamingApiTestWithPixelOutput, MAYBE_EndToEnd) { |
| std::unique_ptr<net::UDPServerSocket> receive_socket( |
| new net::UDPServerSocket(NULL, net::NetLogSource())); |
| receive_socket->AllowAddressReuse(); |
| ASSERT_EQ(net::OK, receive_socket->Listen(GetFreeLocalPort())); |
| net::IPEndPoint receiver_end_point; |
| ASSERT_EQ(net::OK, receive_socket->GetLocalAddress(&receiver_end_point)); |
| receive_socket.reset(); |
| |
| // Start the in-process receiver that examines audio/video for the expected |
| // test patterns. |
| const scoped_refptr<media::cast::StandaloneCastEnvironment> cast_environment( |
| new media::cast::StandaloneCastEnvironment()); |
| TestPatternReceiver* const receiver = |
| new TestPatternReceiver(cast_environment, receiver_end_point); |
| |
| // Launch the page that: 1) renders the source content; 2) uses the |
| // chrome.tabCapture and chrome.cast.streaming APIs to capture its content and |
| // stream using Cast; and 3) calls chrome.test.succeed() once it is |
| // operational. |
| const std::string page_url = base::StringPrintf( |
| "end_to_end_sender.html?port=%d&aesKey=%s&aesIvMask=%s", |
| receiver_end_point.port(), |
| base::HexEncode(receiver->audio_config().aes_key.data(), |
| receiver->audio_config().aes_key.size()).c_str(), |
| base::HexEncode(receiver->audio_config().aes_iv_mask.data(), |
| receiver->audio_config().aes_iv_mask.size()).c_str()); |
| ASSERT_TRUE(RunExtensionSubtest("cast_streaming", page_url)) << message_; |
| |
| // Examine the Cast receiver for expected audio/video test patterns. The |
| // colors and tones specified here must match those in end_to_end_sender.js. |
| // Note that we do not check that the color and tone are received |
| // simultaneously since A/V sync should be measured in perf tests. |
| receiver->AddExpectedTone(200 /* Hz */); |
| receiver->AddExpectedTone(500 /* Hz */); |
| receiver->AddExpectedTone(1800 /* Hz */); |
| receiver->AddExpectedColor(YUVColor(63, 102, 239)); // rgb(255, 0, 0) |
| receiver->AddExpectedColor(YUVColor(173, 41, 26)); // rgb(0, 255, 0) |
| receiver->AddExpectedColor(YUVColor(32, 239, 117)); // rgb(0, 0, 255) |
| receiver->Start(); |
| receiver->WaitForExpectedTonesAndColors(); |
| receiver->Stop(); |
| |
| delete receiver; |
| base::ScopedAllowBlockingForTesting allow_blocking; |
| cast_environment->Shutdown(); |
| } |
| |
| #if !defined(OS_MACOSX) |
| #define MAYBE_RtpStreamError RtpStreamError |
| #else |
| // Flaky on Mac https://crbug.com/841986 |
| #define MAYBE_RtpStreamError DISABLED_RtpStreamError |
| #endif |
| IN_PROC_BROWSER_TEST_P(CastStreamingApiTestWithPixelOutput, |
| MAYBE_RtpStreamError) { |
| ASSERT_TRUE(RunExtensionSubtest("cast_streaming", "rtp_stream_error.html")); |
| } |
| |
| // We run these tests with the audio service both in and out of the the browser |
| // process to have waterfall coverage while the feature rolls out. It should be |
| // removed after launch. Note: CastStreamingApiTestWithPixelOutput.EndToEnd is |
| // the only integration test exercising audio service loopback streams, so it's |
| // a very important test to have. |
| #if (defined(OS_LINUX) && !defined(OS_CHROMEOS)) || defined(OS_MACOSX) || \ |
| defined(OS_WIN) |
| // Supported platforms. |
| INSTANTIATE_TEST_CASE_P(, |
| CastStreamingApiTestWithPixelOutput, |
| ::testing::Bool()); |
| #else |
| // Platforms where the out of process audio service isn't supported |
| INSTANTIATE_TEST_CASE_P(, |
| CastStreamingApiTestWithPixelOutput, |
| ::testing::Values(false)); |
| #endif |
| |
| } // namespace extensions |