Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright 2013 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 #include <algorithm> | |
| 6 #include <cmath> | |
| 7 | |
| 5 #include "base/command_line.h" | 8 #include "base/command_line.h" |
| 9 #include "base/float_util.h" | |
| 10 #include "base/strings/stringprintf.h" | |
| 11 #include "base/synchronization/condition_variable.h" | |
| 12 #include "base/synchronization/lock.h" | |
| 13 #include "base/time/time.h" | |
| 6 #include "chrome/browser/extensions/extension_apitest.h" | 14 #include "chrome/browser/extensions/extension_apitest.h" |
| 7 #include "chrome/common/chrome_switches.h" | 15 #include "chrome/common/chrome_switches.h" |
| 8 #include "content/public/common/content_switches.h" | 16 #include "content/public/common/content_switches.h" |
| 17 #include "media/base/video_frame.h" | |
| 18 #include "media/cast/cast_config.h" | |
| 19 #include "media/cast/cast_environment.h" | |
| 20 #include "media/cast/test/utility/audio_utility.h" | |
| 21 #include "media/cast/test/utility/in_process_receiver.h" | |
| 22 #include "net/base/net_util.h" | |
| 9 #include "testing/gtest/include/gtest/gtest.h" | 23 #include "testing/gtest/include/gtest/gtest.h" |
| 10 | 24 |
| 11 namespace extensions { | 25 namespace extensions { |
| 12 | 26 |
| 13 class CastStreamingApiTest : public ExtensionApiTest { | 27 class CastStreamingApiTest : public ExtensionApiTest { |
| 14 virtual void SetUpCommandLine(CommandLine* command_line) OVERRIDE { | 28 virtual void SetUpCommandLine(CommandLine* command_line) OVERRIDE { |
| 15 ExtensionApiTest::SetUpCommandLine(command_line); | 29 ExtensionApiTest::SetUpCommandLine(command_line); |
| 16 command_line->AppendSwitchASCII( | 30 command_line->AppendSwitchASCII( |
| 17 switches::kWhitelistedExtensionID, | 31 switches::kWhitelistedExtensionID, |
| 18 "ddchlicdkolnonkihahngkmmmjnjlkkf"); | 32 "ddchlicdkolnonkihahngkmmmjnjlkkf"); |
| 19 } | 33 } |
| 20 }; | 34 }; |
| 21 | 35 |
| 22 // Test running the test extension for Cast Mirroring API. | 36 // Test running the test extension for Cast Mirroring API. |
| 23 IN_PROC_BROWSER_TEST_F(CastStreamingApiTest, Basics) { | 37 IN_PROC_BROWSER_TEST_F(CastStreamingApiTest, Basics) { |
| 24 ASSERT_TRUE(RunExtensionSubtest("cast_streaming", "basics.html")); | 38 ASSERT_TRUE(RunExtensionSubtest("cast_streaming", "basics.html")) << message_; |
| 25 } | 39 } |
| 26 | 40 |
| 27 IN_PROC_BROWSER_TEST_F(CastStreamingApiTest, BadLogging) { | 41 IN_PROC_BROWSER_TEST_F(CastStreamingApiTest, BadLogging) { |
| 28 ASSERT_TRUE(RunExtensionSubtest("cast_streaming", "bad_logging.html")); | 42 ASSERT_TRUE(RunExtensionSubtest("cast_streaming", "bad_logging.html")) |
| 43 << message_; | |
| 29 } | 44 } |
| 30 | 45 |
| 46 namespace { | |
| 47 | |
| 48 // An in-process Cast receiver that examines the audio/video frames being | |
| 49 // received for expected colors and tones. Used in | |
| 50 // CastStreamingApiTest.EndToEnd, below. | |
| 51 class TestPatternReceiver : public media::cast::InProcessReceiver { | |
| 52 public: | |
| 53 explicit TestPatternReceiver(const net::IPEndPoint& local_end_point) | |
| 54 : InProcessReceiver( | |
| 55 make_scoped_refptr(new media::cast::CastEnvironment( | |
| 56 media::cast::CastLoggingConfig())), | |
| 57 local_end_point, | |
| 58 net::IPEndPoint(), | |
| 59 media::cast::InProcessReceiver::GetDefaultAudioConfig(), | |
| 60 media::cast::InProcessReceiver::GetDefaultVideoConfig()), | |
| 61 cond_(&lock_), | |
| 62 target_color_y_(0), | |
| 63 target_color_u_(0), | |
| 64 target_color_v_(0), | |
| 65 target_tone_frequency_(0), | |
| 66 current_color_y_(0.0f), | |
| 67 current_color_u_(0.0f), | |
| 68 current_color_v_(0.0f), | |
| 69 current_tone_frequency_(0.0f) {} | |
| 70 virtual ~TestPatternReceiver() {} | |
| 71 | |
| 72 // Blocks the caller until this receiver has seen both |yuv_color| and | |
| 73 // |tone_frequency| consistently for the given |duration|. | |
| 74 bool WaitForColorAndTone(const uint8 yuv_color[3], | |
| 75 int tone_frequency, | |
| 76 base::TimeDelta duration) { | |
| 77 DCHECK(!cast_env()->CurrentlyOn(media::cast::CastEnvironment::MAIN)); | |
| 78 | |
| 79 DVLOG(1) << "Waiting for test pattern: color=yuv(" | |
| 80 << static_cast<int>(yuv_color[0]) << ", " | |
| 81 << static_cast<int>(yuv_color[1]) << ", " | |
| 82 << static_cast<int>(yuv_color[2]) | |
| 83 << "), tone_frequency=" << tone_frequency << " Hz"; | |
| 84 | |
| 85 const base::TimeTicks start_time = cast_env()->Clock()->NowTicks(); | |
| 86 | |
| 87 // Reset target values and counters. | |
| 88 base::AutoLock auto_lock(lock_); // Released by |cond_| in the loop below. | |
| 89 target_color_y_ = yuv_color[0]; | |
| 90 target_color_u_ = yuv_color[1]; | |
| 91 target_color_v_ = yuv_color[2]; | |
| 92 target_tone_frequency_ = tone_frequency; | |
| 93 first_time_near_target_color_ = base::TimeTicks(); | |
| 94 first_time_near_target_tone_ = base::TimeTicks(); | |
| 95 | |
| 96 // Wait until both the color and tone have matched, subject to a timeout. | |
| 97 const int kMaxTotalWaitSeconds = 20; | |
| 98 do { | |
| 99 const base::TimeDelta remaining_wait_time = | |
| 100 base::TimeDelta::FromSeconds(kMaxTotalWaitSeconds) - | |
| 101 (cast_env()->Clock()->NowTicks() - start_time); | |
| 102 if (remaining_wait_time <= base::TimeDelta()) | |
| 103 return false; // Failed to match test pattern within total wait time. | |
| 104 cond_.TimedWait(remaining_wait_time); | |
| 105 | |
| 106 if (!first_time_near_target_color_.is_null() && | |
| 107 !first_time_near_target_tone_.is_null()) { | |
| 108 const base::TimeTicks now = cast_env()->Clock()->NowTicks(); | |
| 109 if ((now - first_time_near_target_color_) >= duration && | |
| 110 (now - first_time_near_target_tone_) >= duration) { | |
| 111 return true; // Successfully matched for sufficient duration. | |
| 112 } | |
| 113 } | |
| 114 } while (true); | |
| 115 } | |
| 116 | |
| 117 private: | |
| 118 // Invoked by InProcessReceiver for each received audio frame. | |
| 119 virtual void OnAudioFrame(scoped_ptr<media::cast::PcmAudioFrame> audio_frame, | |
| 120 const base::TimeTicks& playout_time) OVERRIDE { | |
| 121 DCHECK(cast_env()->CurrentlyOn(media::cast::CastEnvironment::MAIN)); | |
| 122 | |
| 123 if (audio_frame->samples.empty()) { | |
| 124 NOTREACHED() << "OnAudioFrame called with no samples?!?"; | |
| 125 return; | |
| 126 } | |
| 127 | |
| 128 // Assume the audio signal is a single sine wave (it can have some | |
| 129 // low-amplitude noise). Count zero crossings, and extrapolate the | |
| 130 // frequency of the sine wave in |audio_frame|. | |
| 131 const int crossings = media::cast::CountZeroCrossings(audio_frame->samples); | |
| 132 const float seconds_per_frame = audio_frame->samples.size() / | |
| 133 static_cast<float>(audio_frame->frequency); | |
| 134 const float frequency_in_frame = crossings / seconds_per_frame; | |
| 135 | |
| 136 const float kAveragingWeight = 0.1f; | |
| 137 UpdateExponentialMovingAverage( | |
| 138 kAveragingWeight, frequency_in_frame, ¤t_tone_frequency_); | |
| 139 DVLOG(1) << "Current audio tone frequency: " << current_tone_frequency_; | |
| 140 | |
| 141 { | |
| 142 base::AutoLock auto_lock(lock_); | |
|
hubbe
2014/03/04 22:42:26
What is this lock for? Doesn't all of these functi
miu
2014/03/06 06:09:15
No. All CastEnvironment threads are different fro
hubbe
2014/03/06 19:54:43
I missed the exclamation mark in the DCHECK in Wai
| |
| 143 const float kTargetWindowHz = 20; | |
| 144 // Trigger the waiting thread while the current tone is within | |
| 145 // kTargetWindowHz of the target tone. | |
| 146 if (fabsf(current_tone_frequency_ - target_tone_frequency_) < | |
| 147 kTargetWindowHz) { | |
| 148 if (first_time_near_target_tone_.is_null()) | |
| 149 first_time_near_target_tone_ = cast_env()->Clock()->NowTicks(); | |
| 150 cond_.Broadcast(); | |
| 151 } else { | |
| 152 first_time_near_target_tone_ = base::TimeTicks(); | |
| 153 } | |
| 154 } | |
| 155 } | |
| 156 | |
| 157 virtual void OnVideoFrame(const scoped_refptr<media::VideoFrame>& video_frame, | |
| 158 const base::TimeTicks& render_time) OVERRIDE { | |
| 159 DCHECK(cast_env()->CurrentlyOn(media::cast::CastEnvironment::MAIN)); | |
| 160 | |
| 161 CHECK(video_frame->format() == media::VideoFrame::YV12 || | |
| 162 video_frame->format() == media::VideoFrame::I420 || | |
| 163 video_frame->format() == media::VideoFrame::YV12A); | |
| 164 | |
| 165 // Note: We take the median value of each plane because the test image will | |
| 166 // contain mostly a solid color plus some "cruft" which is the "Testing..." | |
| 167 // text in the upper-left corner of the video frame. In other words, we | |
| 168 // want to read "the most common color." | |
| 169 const float kAveragingWeight = 0.2f; | |
| 170 #define UPDATE_FOR_PLANE(which, kXPlane) \ | |
| 171 const uint8 median_##which = ComputeMedianIntensityInPlane( \ | |
| 172 video_frame->row_bytes(media::VideoFrame::kXPlane), \ | |
| 173 video_frame->rows(media::VideoFrame::kXPlane), \ | |
| 174 video_frame->stride(media::VideoFrame::kXPlane), \ | |
| 175 video_frame->data(media::VideoFrame::kXPlane)); \ | |
| 176 UpdateExponentialMovingAverage( \ | |
|
hubbe
2014/03/04 22:42:26
Why do we need a moving average?
A moving average
miu
2014/03/06 06:09:15
Good point. Done.
| |
| 177 kAveragingWeight, median_##which, ¤t_color_##which##_) | |
| 178 | |
| 179 UPDATE_FOR_PLANE(y, kYPlane); | |
|
hubbe
2014/03/04 22:42:26
I think an uint8 medians[3], a const int planes[]
miu
2014/03/06 06:09:15
Done.
| |
| 180 UPDATE_FOR_PLANE(u, kUPlane); | |
| 181 UPDATE_FOR_PLANE(v, kVPlane); | |
| 182 | |
| 183 #undef UPDATE_FOR_PLANE | |
| 184 | |
| 185 DVLOG(1) << "Current video color: yuv(" << current_color_y_ << ", " | |
| 186 << current_color_u_ << ", " << current_color_v_ << ')'; | |
| 187 | |
| 188 { | |
| 189 base::AutoLock auto_lock(lock_); | |
| 190 const float kTargetWindow = 10.0f; | |
| 191 // Trigger the waiting thread while all color channels are within | |
| 192 // kTargetWindow of the target. | |
| 193 if (fabsf(current_color_y_ - target_color_y_) < kTargetWindow && | |
| 194 fabsf(current_color_u_ - target_color_u_) < kTargetWindow && | |
| 195 fabsf(current_color_v_ - target_color_v_) < kTargetWindow) { | |
| 196 if (first_time_near_target_color_.is_null()) | |
| 197 first_time_near_target_color_ = cast_env()->Clock()->NowTicks(); | |
| 198 cond_.Broadcast(); | |
| 199 } else { | |
| 200 first_time_near_target_color_ = base::TimeTicks(); | |
| 201 } | |
| 202 } | |
| 203 } | |
| 204 | |
| 205 static void UpdateExponentialMovingAverage(float weight, | |
| 206 float sample_value, | |
| 207 float* average) { | |
| 208 *average += weight * sample_value - weight * (*average); | |
|
hubbe
2014/03/04 22:42:26
Shouldn't this be:
*average += weight * sample_val
miu
2014/03/06 06:09:15
Note: I'm using "*average += ..." and not "*averag
hubbe
2014/03/06 19:54:43
Ah tricky. Seems harder to read that way though.
miu
2014/03/07 22:40:29
Changed.
| |
| 209 CHECK(base::IsFinite(*average)); | |
| 210 } | |
| 211 | |
| 212 static uint8 ComputeMedianIntensityInPlane(int width, | |
| 213 int height, | |
| 214 int stride, | |
| 215 uint8* data) { | |
| 216 const int num_pixels = width * height; | |
| 217 if (num_pixels <= 0) | |
| 218 return 0; | |
| 219 // If necessary, re-pack the pixels such that the stride is equal to the | |
| 220 // width. | |
| 221 if (width < stride) { | |
| 222 for (int y = 1; y < height; ++y) { | |
| 223 uint8* const src = data + y * stride; | |
| 224 uint8* const dest = data + y * width; | |
| 225 memmove(dest, src, width); | |
|
hubbe
2014/03/04 22:42:26
Are you sure we're actually allowed to modify the
miu
2014/03/06 06:09:15
Yes. There is no other reader of the frame.
hubbe
2014/03/06 19:54:43
I was more concerned about the vp8 library using t
| |
| 226 } | |
| 227 } | |
| 228 const size_t middle_idx = num_pixels / 2; | |
| 229 std::nth_element(data, data + middle_idx, data + num_pixels); | |
| 230 return data[middle_idx]; | |
| 231 } | |
| 232 | |
| 233 base::Lock lock_; | |
| 234 base::ConditionVariable cond_; | |
| 235 | |
| 236 float target_color_y_; | |
| 237 float target_color_u_; | |
| 238 float target_color_v_; | |
| 239 float target_tone_frequency_; | |
| 240 | |
| 241 float current_color_y_; | |
| 242 float current_color_u_; | |
| 243 float current_color_v_; | |
| 244 base::TimeTicks first_time_near_target_color_; | |
| 245 float current_tone_frequency_; | |
| 246 base::TimeTicks first_time_near_target_tone_; | |
| 247 | |
| 248 DISALLOW_COPY_AND_ASSIGN(TestPatternReceiver); | |
| 249 }; | |
| 250 | |
| 251 } // namespace | |
| 252 | |
| 253 class CastStreamingApiTestWithPixelOutput : public CastStreamingApiTest { | |
| 254 virtual void SetUp() OVERRIDE { | |
| 255 if (!UsingOSMesa()) | |
| 256 EnablePixelOutput(); | |
| 257 CastStreamingApiTest::SetUp(); | |
| 258 } | |
| 259 }; | |
| 260 | |
| 261 // Tests the Cast streaming API and its basic functionality end-to-end. An | |
| 262 // extension subtest is run to generate test content, capture that content, and | |
| 263 // use the API to send it out. At the same time, this test launches an | |
| 264 // in-process Cast receiver, listening on a localhost UDP socket, to receive the | |
| 265 // content and check whether it matches expectations. | |
| 266 // | |
| 267 // Note: This test is disabled until outstanding bugs are fixed and the | |
| 268 // media/cast library has achieved sufficient stability. | |
|
hubbe
2014/03/04 22:42:26
Maybe file a bug too?
miu
2014/03/06 06:09:15
Done.
| |
| 269 IN_PROC_BROWSER_TEST_F(CastStreamingApiTestWithPixelOutput, DISABLED_EndToEnd) { | |
| 270 // This test is too slow to succeed with OSMesa on the bots. | |
| 271 if (UsingOSMesa()) { | |
| 272 LOG(WARNING) << "Skipping this test since OSMesa is too slow on the bots."; | |
| 273 return; | |
| 274 } | |
| 275 | |
| 276 // Determine a unused UDP port for the in-process receiver to listen on, in | |
| 277 // the range [2300,2345]. We utilize the hero super-power known as "crossing | |
| 278 // our fingers" to find an unused port. Note: As of this writing, the cast | |
| 279 // sender runs on port 2346. | |
|
hubbe
2014/03/04 22:42:26
This is not a good way to do it.
We should create
miu
2014/03/06 06:09:15
Done. I had looked several weeks ago for the help
| |
| 280 net::IPAddressNumber localhost; | |
| 281 localhost.push_back(127); | |
| 282 localhost.push_back(0); | |
| 283 localhost.push_back(0); | |
| 284 localhost.push_back(1); | |
| 285 const int64 random_temporal_offset = | |
| 286 (base::TimeTicks::Now() - base::TimeTicks::UnixEpoch()).InMilliseconds() / | |
| 287 10; | |
| 288 const int hopefully_unused_receiver_port = 2300 + random_temporal_offset % 46; | |
| 289 | |
| 290 // Start the in-process receiver that examines audio/video for the expected | |
| 291 // test patterns. | |
| 292 TestPatternReceiver receiver( | |
| 293 net::IPEndPoint(localhost, hopefully_unused_receiver_port)); | |
| 294 receiver.Start(); | |
| 295 | |
| 296 // Launch the page that: 1) renders the source content; 2) uses the | |
| 297 // chrome.tabCapture and chrome.cast.streaming APIs to capture its content and | |
| 298 // stream using Cast; and 3) calls chrome.test.succeed() once it is | |
| 299 // operational. | |
| 300 const std::string& page_url = base::StringPrintf( | |
| 301 "end_to_end_sender.html?port=%d", hopefully_unused_receiver_port); | |
| 302 ASSERT_TRUE(RunExtensionSubtest("cast_streaming", page_url)) << message_; | |
| 303 | |
| 304 // Examine the Cast receiver for expected audio/video test patterns. The | |
| 305 // colors and tones specified here must match those in end_to_end_sender.js. | |
| 306 const uint8 kRedInYUV[3] = {82, 90, 240}; // rgb(255, 0, 0) | |
| 307 const uint8 kGreenInYUV[3] = {145, 54, 34}; // rgb(0, 255, 0) | |
| 308 const uint8 kBlueInYUV[3] = {41, 240, 110}; // rgb(0, 0, 255) | |
| 309 const base::TimeDelta kOneHalfSecond = base::TimeDelta::FromMilliseconds(500); | |
| 310 EXPECT_TRUE( | |
| 311 receiver.WaitForColorAndTone(kRedInYUV, 200 /* Hz */, kOneHalfSecond)); | |
| 312 EXPECT_TRUE( | |
| 313 receiver.WaitForColorAndTone(kGreenInYUV, 500 /* Hz */, kOneHalfSecond)); | |
| 314 EXPECT_TRUE( | |
| 315 receiver.WaitForColorAndTone(kBlueInYUV, 1800 /* Hz */, kOneHalfSecond)); | |
| 316 } | |
| 317 | |
| 31 } // namespace extensions | 318 } // namespace extensions |
| OLD | NEW |