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 |