OLD | NEW |
(Empty) | |
| 1 // Copyright 2013 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 #include <algorithm> |
| 6 #include <cmath> |
| 7 |
| 8 #include "base/command_line.h" |
| 9 #include "base/float_util.h" |
| 10 #include "base/run_loop.h" |
| 11 #include "base/strings/stringprintf.h" |
| 12 #include "base/synchronization/lock.h" |
| 13 #include "base/time/time.h" |
| 14 #include "chrome/browser/extensions/extension_apitest.h" |
| 15 #include "chrome/common/chrome_switches.h" |
| 16 #include "content/public/common/content_switches.h" |
| 17 #include "media/base/bind_to_current_loop.h" |
| 18 #include "media/base/video_frame.h" |
| 19 #include "media/cast/cast_config.h" |
| 20 #include "media/cast/cast_environment.h" |
| 21 #include "media/cast/test/utility/audio_utility.h" |
| 22 #include "media/cast/test/utility/default_config.h" |
| 23 #include "media/cast/test/utility/in_process_receiver.h" |
| 24 #include "media/cast/test/utility/standalone_cast_environment.h" |
| 25 #include "net/base/net_errors.h" |
| 26 #include "net/base/net_util.h" |
| 27 #include "net/base/rand_callback.h" |
| 28 #include "net/udp/udp_socket.h" |
| 29 #include "testing/gtest/include/gtest/gtest.h" |
| 30 |
| 31 namespace extensions { |
| 32 |
| 33 class CastStreamingApiTest : public ExtensionApiTest { |
| 34 public: |
| 35 virtual void SetUpCommandLine(CommandLine* command_line) OVERRIDE { |
| 36 ExtensionApiTest::SetUpCommandLine(command_line); |
| 37 command_line->AppendSwitchASCII(switches::kWhitelistedExtensionID, |
| 38 "ddchlicdkolnonkihahngkmmmjnjlkkf"); |
| 39 } |
| 40 }; |
| 41 |
| 42 // Test running the test extension for Cast Mirroring API. |
| 43 IN_PROC_BROWSER_TEST_F(CastStreamingApiTest, Basics) { |
| 44 ASSERT_TRUE(RunExtensionSubtest("cast_streaming", "basics.html")) << message_; |
| 45 } |
| 46 |
| 47 IN_PROC_BROWSER_TEST_F(CastStreamingApiTest, Stats) { |
| 48 ASSERT_TRUE(RunExtensionSubtest("cast_streaming", "stats.html")) << message_; |
| 49 } |
| 50 |
| 51 IN_PROC_BROWSER_TEST_F(CastStreamingApiTest, BadLogging) { |
| 52 ASSERT_TRUE(RunExtensionSubtest("cast_streaming", "bad_logging.html")) |
| 53 << message_; |
| 54 } |
| 55 |
| 56 namespace { |
| 57 |
| 58 // An in-process Cast receiver that examines the audio/video frames being |
| 59 // received for expected colors and tones. Used in |
| 60 // CastStreamingApiTest.EndToEnd, below. |
| 61 class TestPatternReceiver : public media::cast::InProcessReceiver { |
| 62 public: |
| 63 explicit TestPatternReceiver( |
| 64 const scoped_refptr<media::cast::CastEnvironment>& cast_environment, |
| 65 const net::IPEndPoint& local_end_point) |
| 66 : InProcessReceiver(cast_environment, |
| 67 local_end_point, |
| 68 net::IPEndPoint(), |
| 69 media::cast::GetDefaultAudioReceiverConfig(), |
| 70 media::cast::GetDefaultVideoReceiverConfig()), |
| 71 target_tone_frequency_(0), |
| 72 current_tone_frequency_(0.0f) { |
| 73 memset(&target_color_, 0, sizeof(target_color_)); |
| 74 memset(¤t_color_, 0, sizeof(current_color_)); |
| 75 } |
| 76 |
| 77 virtual ~TestPatternReceiver() {} |
| 78 |
| 79 // Blocks the caller until this receiver has seen both |yuv_color| and |
| 80 // |tone_frequency| consistently for the given |duration|. |
| 81 void WaitForColorAndTone(const uint8 yuv_color[3], |
| 82 int tone_frequency, |
| 83 base::TimeDelta duration) { |
| 84 LOG(INFO) << "Waiting for test pattern: color=yuv(" |
| 85 << static_cast<int>(yuv_color[0]) << ", " |
| 86 << static_cast<int>(yuv_color[1]) << ", " |
| 87 << static_cast<int>(yuv_color[2]) |
| 88 << "), tone_frequency=" << tone_frequency << " Hz"; |
| 89 |
| 90 base::RunLoop run_loop; |
| 91 cast_env()->PostTask( |
| 92 media::cast::CastEnvironment::MAIN, |
| 93 FROM_HERE, |
| 94 base::Bind(&TestPatternReceiver::NotifyOnceMatched, |
| 95 base::Unretained(this), |
| 96 yuv_color, |
| 97 tone_frequency, |
| 98 duration, |
| 99 media::BindToCurrentLoop(run_loop.QuitClosure()))); |
| 100 run_loop.Run(); |
| 101 } |
| 102 |
| 103 private: |
| 104 // Resets tracking data and sets the match duration and callback. |
| 105 void NotifyOnceMatched(const uint8 yuv_color[3], |
| 106 int tone_frequency, |
| 107 base::TimeDelta match_duration, |
| 108 const base::Closure& matched_callback) { |
| 109 DCHECK(cast_env()->CurrentlyOn(media::cast::CastEnvironment::MAIN)); |
| 110 |
| 111 match_duration_ = match_duration; |
| 112 matched_callback_ = matched_callback; |
| 113 target_color_[0] = yuv_color[0]; |
| 114 target_color_[1] = yuv_color[1]; |
| 115 target_color_[2] = yuv_color[2]; |
| 116 target_tone_frequency_ = tone_frequency; |
| 117 first_time_near_target_color_ = base::TimeTicks(); |
| 118 first_time_near_target_tone_ = base::TimeTicks(); |
| 119 } |
| 120 |
| 121 // Runs |matched_callback_| once both color and tone have been matched for the |
| 122 // required |match_duration_|. |
| 123 void NotifyIfMatched() { |
| 124 DCHECK(cast_env()->CurrentlyOn(media::cast::CastEnvironment::MAIN)); |
| 125 |
| 126 // TODO(miu): Check audio tone too, once audio is fixed in the library. |
| 127 // http://crbug.com/349295 |
| 128 if (first_time_near_target_color_.is_null() || |
| 129 /*first_time_near_target_tone_.is_null()*/ false) |
| 130 return; |
| 131 const base::TimeTicks now = cast_env()->Clock()->NowTicks(); |
| 132 if ((now - first_time_near_target_color_) >= match_duration_ && |
| 133 /*(now - first_time_near_target_tone_) >= match_duration_*/ true) { |
| 134 matched_callback_.Run(); |
| 135 } |
| 136 } |
| 137 |
| 138 // Invoked by InProcessReceiver for each received audio frame. |
| 139 virtual void OnAudioFrame(scoped_ptr<media::cast::PcmAudioFrame> audio_frame, |
| 140 const base::TimeTicks& playout_time) OVERRIDE { |
| 141 DCHECK(cast_env()->CurrentlyOn(media::cast::CastEnvironment::MAIN)); |
| 142 |
| 143 if (audio_frame->samples.empty()) { |
| 144 NOTREACHED() << "OnAudioFrame called with no samples?!?"; |
| 145 return; |
| 146 } |
| 147 |
| 148 // Assume the audio signal is a single sine wave (it can have some |
| 149 // low-amplitude noise). Count zero crossings, and extrapolate the |
| 150 // frequency of the sine wave in |audio_frame|. |
| 151 const int crossings = media::cast::CountZeroCrossings(audio_frame->samples); |
| 152 const float seconds_per_frame = audio_frame->samples.size() / |
| 153 static_cast<float>(audio_frame->frequency); |
| 154 const float frequency_in_frame = crossings / seconds_per_frame; |
| 155 |
| 156 const float kAveragingWeight = 0.1f; |
| 157 UpdateExponentialMovingAverage( |
| 158 kAveragingWeight, frequency_in_frame, ¤t_tone_frequency_); |
| 159 VLOG(1) << "Current audio tone frequency: " << current_tone_frequency_; |
| 160 |
| 161 const float kTargetWindowHz = 20; |
| 162 // Update the time at which the current tone started falling within |
| 163 // kTargetWindowHz of the target tone. |
| 164 if (fabsf(current_tone_frequency_ - target_tone_frequency_) < |
| 165 kTargetWindowHz) { |
| 166 if (first_time_near_target_tone_.is_null()) |
| 167 first_time_near_target_tone_ = cast_env()->Clock()->NowTicks(); |
| 168 NotifyIfMatched(); |
| 169 } else { |
| 170 first_time_near_target_tone_ = base::TimeTicks(); |
| 171 } |
| 172 } |
| 173 |
| 174 virtual void OnVideoFrame(const scoped_refptr<media::VideoFrame>& video_frame, |
| 175 const base::TimeTicks& render_time) OVERRIDE { |
| 176 DCHECK(cast_env()->CurrentlyOn(media::cast::CastEnvironment::MAIN)); |
| 177 |
| 178 CHECK(video_frame->format() == media::VideoFrame::YV12 || |
| 179 video_frame->format() == media::VideoFrame::I420 || |
| 180 video_frame->format() == media::VideoFrame::YV12A); |
| 181 |
| 182 // Note: We take the median value of each plane because the test image will |
| 183 // contain mostly a solid color plus some "cruft" which is the "Testing..." |
| 184 // text in the upper-left corner of the video frame. In other words, we |
| 185 // want to read "the most common color." |
| 186 const int kPlanes[] = {media::VideoFrame::kYPlane, |
| 187 media::VideoFrame::kUPlane, |
| 188 media::VideoFrame::kVPlane}; |
| 189 for (size_t i = 0; i < arraysize(kPlanes); ++i) { |
| 190 current_color_[i] = |
| 191 ComputeMedianIntensityInPlane(video_frame->row_bytes(kPlanes[i]), |
| 192 video_frame->rows(kPlanes[i]), |
| 193 video_frame->stride(kPlanes[i]), |
| 194 video_frame->data(kPlanes[i])); |
| 195 } |
| 196 |
| 197 VLOG(1) << "Current video color: yuv(" << current_color_[0] << ", " |
| 198 << current_color_[1] << ", " << current_color_[2] << ')'; |
| 199 |
| 200 const float kTargetWindow = 10.0f; |
| 201 // Update the time at which all color channels started falling within |
| 202 // kTargetWindow of the target. |
| 203 if (fabsf(current_color_[0] - target_color_[0]) < kTargetWindow && |
| 204 fabsf(current_color_[1] - target_color_[1]) < kTargetWindow && |
| 205 fabsf(current_color_[2] - target_color_[2]) < kTargetWindow) { |
| 206 if (first_time_near_target_color_.is_null()) |
| 207 first_time_near_target_color_ = cast_env()->Clock()->NowTicks(); |
| 208 NotifyIfMatched(); |
| 209 } else { |
| 210 first_time_near_target_color_ = base::TimeTicks(); |
| 211 } |
| 212 } |
| 213 |
| 214 static void UpdateExponentialMovingAverage(float weight, |
| 215 float sample_value, |
| 216 float* average) { |
| 217 *average = weight * sample_value + (1.0f - weight) * (*average); |
| 218 CHECK(base::IsFinite(*average)); |
| 219 } |
| 220 |
| 221 static uint8 ComputeMedianIntensityInPlane(int width, |
| 222 int height, |
| 223 int stride, |
| 224 uint8* data) { |
| 225 const int num_pixels = width * height; |
| 226 if (num_pixels <= 0) |
| 227 return 0; |
| 228 // If necessary, re-pack the pixels such that the stride is equal to the |
| 229 // width. |
| 230 if (width < stride) { |
| 231 for (int y = 1; y < height; ++y) { |
| 232 uint8* const src = data + y * stride; |
| 233 uint8* const dest = data + y * width; |
| 234 memmove(dest, src, width); |
| 235 } |
| 236 } |
| 237 const size_t middle_idx = num_pixels / 2; |
| 238 std::nth_element(data, data + middle_idx, data + num_pixels); |
| 239 return data[middle_idx]; |
| 240 } |
| 241 |
| 242 base::TimeDelta match_duration_; |
| 243 base::Closure matched_callback_; |
| 244 |
| 245 float target_color_[3]; // Y, U, V |
| 246 float target_tone_frequency_; |
| 247 |
| 248 float current_color_[3]; // Y, U, V |
| 249 base::TimeTicks first_time_near_target_color_; |
| 250 float current_tone_frequency_; |
| 251 base::TimeTicks first_time_near_target_tone_; |
| 252 |
| 253 DISALLOW_COPY_AND_ASSIGN(TestPatternReceiver); |
| 254 }; |
| 255 |
| 256 } // namespace |
| 257 |
| 258 class CastStreamingApiTestWithPixelOutput : public CastStreamingApiTest { |
| 259 virtual void SetUp() OVERRIDE { |
| 260 EnablePixelOutput(); |
| 261 CastStreamingApiTest::SetUp(); |
| 262 } |
| 263 |
| 264 virtual void SetUpCommandLine(CommandLine* command_line) OVERRIDE { |
| 265 command_line->AppendSwitchASCII(switches::kWindowSize, "128,128"); |
| 266 CastStreamingApiTest::SetUpCommandLine(command_line); |
| 267 } |
| 268 }; |
| 269 |
| 270 // Tests the Cast streaming API and its basic functionality end-to-end. An |
| 271 // extension subtest is run to generate test content, capture that content, and |
| 272 // use the API to send it out. At the same time, this test launches an |
| 273 // in-process Cast receiver, listening on a localhost UDP socket, to receive the |
| 274 // content and check whether it matches expectations. |
| 275 // |
| 276 // Note: This test is disabled until outstanding bugs are fixed and the |
| 277 // media/cast library has achieved sufficient stability. |
| 278 // http://crbug.com/349599 |
| 279 IN_PROC_BROWSER_TEST_F(CastStreamingApiTestWithPixelOutput, DISABLED_EndToEnd) { |
| 280 // Determine a unused UDP port for the in-process receiver to listen on. |
| 281 // Method: Bind a UDP socket on port 0, and then check which port the |
| 282 // operating system assigned to it. |
| 283 net::IPAddressNumber localhost; |
| 284 localhost.push_back(127); |
| 285 localhost.push_back(0); |
| 286 localhost.push_back(0); |
| 287 localhost.push_back(1); |
| 288 scoped_ptr<net::UDPSocket> receive_socket( |
| 289 new net::UDPSocket(net::DatagramSocket::DEFAULT_BIND, |
| 290 net::RandIntCallback(), |
| 291 NULL, |
| 292 net::NetLog::Source())); |
| 293 receive_socket->AllowAddressReuse(); |
| 294 ASSERT_EQ(net::OK, receive_socket->Bind(net::IPEndPoint(localhost, 0))); |
| 295 net::IPEndPoint receiver_end_point; |
| 296 ASSERT_EQ(net::OK, receive_socket->GetLocalAddress(&receiver_end_point)); |
| 297 receive_socket.reset(); |
| 298 |
| 299 // Start the in-process receiver that examines audio/video for the expected |
| 300 // test patterns. |
| 301 const scoped_refptr<media::cast::StandaloneCastEnvironment> cast_environment( |
| 302 new media::cast::StandaloneCastEnvironment( |
| 303 media::cast::CastLoggingConfig())); |
| 304 TestPatternReceiver* const receiver = |
| 305 new TestPatternReceiver(cast_environment, receiver_end_point); |
| 306 receiver->Start(); |
| 307 |
| 308 // Launch the page that: 1) renders the source content; 2) uses the |
| 309 // chrome.tabCapture and chrome.cast.streaming APIs to capture its content and |
| 310 // stream using Cast; and 3) calls chrome.test.succeed() once it is |
| 311 // operational. |
| 312 const std::string page_url = base::StringPrintf( |
| 313 "end_to_end_sender.html?port=%d", receiver_end_point.port()); |
| 314 ASSERT_TRUE(RunExtensionSubtest("cast_streaming", page_url)) << message_; |
| 315 |
| 316 // Examine the Cast receiver for expected audio/video test patterns. The |
| 317 // colors and tones specified here must match those in end_to_end_sender.js. |
| 318 const uint8 kRedInYUV[3] = {82, 90, 240}; // rgb(255, 0, 0) |
| 319 const uint8 kGreenInYUV[3] = {145, 54, 34}; // rgb(0, 255, 0) |
| 320 const uint8 kBlueInYUV[3] = {41, 240, 110}; // rgb(0, 0, 255) |
| 321 const base::TimeDelta kOneHalfSecond = base::TimeDelta::FromMilliseconds(500); |
| 322 receiver->WaitForColorAndTone(kRedInYUV, 200 /* Hz */, kOneHalfSecond); |
| 323 receiver->WaitForColorAndTone(kGreenInYUV, 500 /* Hz */, kOneHalfSecond); |
| 324 receiver->WaitForColorAndTone(kBlueInYUV, 1800 /* Hz */, kOneHalfSecond); |
| 325 |
| 326 // TODO(miu): Uncomment once GetWeakPtr() NULL crash in PacedSender is fixed |
| 327 // (see http://crbug.com/349786): |
| 328 // receiver->DestroySoon(); |
| 329 cast_environment->Shutdown(); |
| 330 } |
| 331 |
| 332 } // namespace extensions |
OLD | NEW |