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.
9 #include "base/callback_helpers.h"
10 #include "base/command_line.h"
11 #include "base/memory/scoped_ptr.h"
12 #include "base/run_loop.h"
13 #include "base/strings/string_number_conversions.h"
14 #include "base/strings/stringprintf.h"
15 #include "chrome/browser/extensions/extension_apitest.h"
16 #include "chrome/common/chrome_switches.h"
17 #include "content/public/common/content_switches.h"
18 #include "extensions/common/switches.h"
19 #include "media/base/bind_to_current_loop.h"
20 #include "media/base/video_frame.h"
21 #include "media/cast/cast_config.h"
22 #include "media/cast/cast_environment.h"
23 #include "media/cast/test/utility/audio_utility.h"
24 #include "media/cast/test/utility/default_config.h"
25 #include "media/cast/test/utility/in_process_receiver.h"
26 #include "media/cast/test/utility/net_utility.h"
27 #include "media/cast/test/utility/standalone_cast_environment.h"
28 #include "net/base/net_errors.h"
29 #include "net/base/net_util.h"
30 #include "net/base/rand_callback.h"
31 #include "net/udp/udp_server_socket.h"
32 #include "testing/gtest/include/gtest/gtest.h"
34 using media::cast::test::GetFreeLocalPort
;
36 namespace extensions
{
38 class CastStreamingApiTest
: public ExtensionApiTest
{
40 void SetUpCommandLine(base::CommandLine
* command_line
) override
{
41 ExtensionApiTest::SetUpCommandLine(command_line
);
42 command_line
->AppendSwitchASCII(
43 extensions::switches::kWhitelistedExtensionID
,
44 "ddchlicdkolnonkihahngkmmmjnjlkkf");
45 command_line
->AppendSwitchASCII(::switches::kWindowSize
, "300,300");
49 // Test running the test extension for Cast Mirroring API.
50 IN_PROC_BROWSER_TEST_F(CastStreamingApiTest
, Basics
) {
51 ASSERT_TRUE(RunExtensionSubtest("cast_streaming", "basics.html")) << message_
;
54 IN_PROC_BROWSER_TEST_F(CastStreamingApiTest
, Stats
) {
55 ASSERT_TRUE(RunExtensionSubtest("cast_streaming", "stats.html")) << message_
;
58 IN_PROC_BROWSER_TEST_F(CastStreamingApiTest
, BadLogging
) {
59 ASSERT_TRUE(RunExtensionSubtest("cast_streaming", "bad_logging.html"))
63 IN_PROC_BROWSER_TEST_F(CastStreamingApiTest
, DestinationNotSet
) {
64 ASSERT_TRUE(RunExtensionSubtest("cast_streaming", "destination_not_set.html"))
68 IN_PROC_BROWSER_TEST_F(CastStreamingApiTest
, StopNoStart
) {
69 ASSERT_TRUE(RunExtensionSubtest("cast_streaming", "stop_no_start.html"))
73 IN_PROC_BROWSER_TEST_F(CastStreamingApiTest
, NullStream
) {
74 ASSERT_TRUE(RunExtensionSubtest("cast_streaming", "null_stream.html"))
85 YUVColor() : y(0), u(0), v(0) {}
86 YUVColor(int y_val
, int u_val
, int v_val
) : y(y_val
), u(u_val
), v(v_val
) {}
90 media::cast::FrameReceiverConfig
WithFakeAesKeyAndIv(
91 media::cast::FrameReceiverConfig config
) {
92 config
.aes_key
= "0123456789abcdef";
93 config
.aes_iv_mask
= "fedcba9876543210";
97 // An in-process Cast receiver that examines the audio/video frames being
98 // received for expected colors and tones. Used in
99 // CastStreamingApiTest.EndToEnd, below.
100 class TestPatternReceiver
: public media::cast::InProcessReceiver
{
102 explicit TestPatternReceiver(
103 const scoped_refptr
<media::cast::CastEnvironment
>& cast_environment
,
104 const net::IPEndPoint
& local_end_point
)
109 WithFakeAesKeyAndIv(media::cast::GetDefaultAudioReceiverConfig()),
110 WithFakeAesKeyAndIv(media::cast::GetDefaultVideoReceiverConfig())) {
113 ~TestPatternReceiver() override
{}
115 void AddExpectedTone(int tone_frequency
) {
116 expected_tones_
.push_back(tone_frequency
);
119 void AddExpectedColor(const YUVColor
& yuv_color
) {
120 expected_yuv_colors_
.push_back(yuv_color
);
123 // Blocks the caller until all expected tones and colors have been observed.
124 void WaitForExpectedTonesAndColors() {
125 base::RunLoop run_loop
;
126 cast_env()->PostTask(
127 media::cast::CastEnvironment::MAIN
,
129 base::Bind(&TestPatternReceiver::NotifyOnceObservedAllTonesAndColors
,
130 base::Unretained(this),
131 media::BindToCurrentLoop(run_loop
.QuitClosure())));
136 void NotifyOnceObservedAllTonesAndColors(const base::Closure
& done_callback
) {
137 DCHECK(cast_env()->CurrentlyOn(media::cast::CastEnvironment::MAIN
));
138 done_callback_
= done_callback
;
139 MaybeRunDoneCallback();
142 void MaybeRunDoneCallback() {
143 DCHECK(cast_env()->CurrentlyOn(media::cast::CastEnvironment::MAIN
));
144 if (done_callback_
.is_null())
146 if (expected_tones_
.empty() && expected_yuv_colors_
.empty()) {
147 base::ResetAndReturn(&done_callback_
).Run();
149 LOG(INFO
) << "Waiting to encounter " << expected_tones_
.size()
150 << " more tone(s) and " << expected_yuv_colors_
.size()
151 << " more color(s).";
155 // Invoked by InProcessReceiver for each received audio frame.
156 void OnAudioFrame(scoped_ptr
<media::AudioBus
> audio_frame
,
157 const base::TimeTicks
& playout_time
,
158 bool is_continuous
) override
{
159 DCHECK(cast_env()->CurrentlyOn(media::cast::CastEnvironment::MAIN
));
161 if (audio_frame
->frames() <= 0) {
162 NOTREACHED() << "OnAudioFrame called with no samples?!?";
166 if (done_callback_
.is_null() || expected_tones_
.empty())
167 return; // No need to waste CPU doing analysis on the signal.
169 // Assume the audio signal is a single sine wave (it can have some
170 // low-amplitude noise). Count zero crossings, and extrapolate the
171 // frequency of the sine wave in |audio_frame|.
173 for (int ch
= 0; ch
< audio_frame
->channels(); ++ch
) {
174 crossings
+= media::cast::CountZeroCrossings(audio_frame
->channel(ch
),
175 audio_frame
->frames());
177 crossings
/= audio_frame
->channels(); // Take the average.
178 const float seconds_per_frame
=
179 audio_frame
->frames() / static_cast<float>(audio_config().rtp_timebase
);
180 const float frequency
= crossings
/ seconds_per_frame
/ 2.0f
;
181 VLOG(1) << "Current audio tone frequency: " << frequency
;
183 const int kTargetWindowHz
= 20;
184 for (std::vector
<int>::iterator it
= expected_tones_
.begin();
185 it
!= expected_tones_
.end(); ++it
) {
186 if (abs(static_cast<int>(frequency
) - *it
) < kTargetWindowHz
) {
187 LOG(INFO
) << "Heard tone at frequency " << *it
<< " Hz.";
188 expected_tones_
.erase(it
);
189 MaybeRunDoneCallback();
195 void OnVideoFrame(const scoped_refptr
<media::VideoFrame
>& video_frame
,
196 const base::TimeTicks
& playout_time
,
197 bool is_continuous
) override
{
198 DCHECK(cast_env()->CurrentlyOn(media::cast::CastEnvironment::MAIN
));
200 CHECK(video_frame
->format() == media::PIXEL_FORMAT_YV12
||
201 video_frame
->format() == media::PIXEL_FORMAT_I420
||
202 video_frame
->format() == media::PIXEL_FORMAT_YV12A
);
204 if (done_callback_
.is_null() || expected_yuv_colors_
.empty())
205 return; // No need to waste CPU doing analysis on the frame.
207 // Take the median value of each plane because the test image will contain a
208 // letterboxed content region of mostly a solid color plus a small piece of
209 // "something" that's animating to keep the tab capture pipeline generating
211 const gfx::Rect region
= FindLetterboxedContentRegion(video_frame
.get());
212 YUVColor current_color
;
213 current_color
.y
= ComputeMedianIntensityInRegionInPlane(
215 video_frame
->stride(media::VideoFrame::kYPlane
),
216 video_frame
->data(media::VideoFrame::kYPlane
));
217 current_color
.u
= ComputeMedianIntensityInRegionInPlane(
218 gfx::ScaleToEnclosedRect(region
, 0.5f
),
219 video_frame
->stride(media::VideoFrame::kUPlane
),
220 video_frame
->data(media::VideoFrame::kUPlane
));
221 current_color
.v
= ComputeMedianIntensityInRegionInPlane(
222 gfx::ScaleToEnclosedRect(region
, 0.5f
),
223 video_frame
->stride(media::VideoFrame::kVPlane
),
224 video_frame
->data(media::VideoFrame::kVPlane
));
225 VLOG(1) << "Current video color: yuv(" << current_color
.y
<< ", "
226 << current_color
.u
<< ", " << current_color
.v
<< ')';
228 const int kTargetWindow
= 10;
229 for (std::vector
<YUVColor
>::iterator it
= expected_yuv_colors_
.begin();
230 it
!= expected_yuv_colors_
.end(); ++it
) {
231 if (abs(current_color
.y
- it
->y
) < kTargetWindow
&&
232 abs(current_color
.u
- it
->u
) < kTargetWindow
&&
233 abs(current_color
.v
- it
->v
) < kTargetWindow
) {
234 LOG(INFO
) << "Saw color yuv(" << it
->y
<< ", " << it
->u
<< ", "
236 expected_yuv_colors_
.erase(it
);
237 MaybeRunDoneCallback();
243 // Return the region that excludes the black letterboxing borders surrounding
244 // the content within |frame|, if any.
245 static gfx::Rect
FindLetterboxedContentRegion(
246 const media::VideoFrame
* frame
) {
247 const int kNonBlackIntensityThreshold
= 20; // 16 plus some fuzz.
248 const int width
= frame
->row_bytes(media::VideoFrame::kYPlane
);
249 const int height
= frame
->rows(media::VideoFrame::kYPlane
);
250 const int stride
= frame
->stride(media::VideoFrame::kYPlane
);
254 // Scan from the bottom-right until the first non-black pixel is
256 for (int y
= height
- 1; y
>= 0; --y
) {
257 const uint8
* const start
=
258 frame
->data(media::VideoFrame::kYPlane
) + y
* stride
;
259 const uint8
* const end
= start
+ width
;
260 for (const uint8
* p
= end
- 1; p
>= start
; --p
) {
261 if (*p
> kNonBlackIntensityThreshold
) {
262 result
.set_width(p
- start
+ 1);
263 result
.set_height(y
+ 1);
264 y
= 0; // Discontinue outer loop.
270 // Scan from the upper-left until the first non-black pixel is encountered.
271 for (int y
= 0; y
< result
.height(); ++y
) {
272 const uint8
* const start
=
273 frame
->data(media::VideoFrame::kYPlane
) + y
* stride
;
274 const uint8
* const end
= start
+ result
.width();
275 for (const uint8
* p
= start
; p
< end
; ++p
) {
276 if (*p
> kNonBlackIntensityThreshold
) {
277 result
.set_x(p
- start
);
278 result
.set_width(result
.width() - result
.x());
280 result
.set_height(result
.height() - result
.y());
281 y
= result
.height(); // Discontinue outer loop.
290 static uint8
ComputeMedianIntensityInRegionInPlane(const gfx::Rect
& region
,
293 if (region
.IsEmpty())
295 const size_t num_values
= region
.size().GetArea();
296 scoped_ptr
<uint8
[]> values(new uint8
[num_values
]);
297 for (int y
= 0; y
< region
.height(); ++y
) {
298 memcpy(values
.get() + y
* region
.width(),
299 data
+ (region
.y() + y
) * stride
+ region
.x(),
302 const size_t middle_idx
= num_values
/ 2;
303 std::nth_element(values
.get(),
304 values
.get() + middle_idx
,
305 values
.get() + num_values
);
306 return values
[middle_idx
];
309 std::vector
<int> expected_tones_
;
310 std::vector
<YUVColor
> expected_yuv_colors_
;
311 base::Closure done_callback_
;
313 DISALLOW_COPY_AND_ASSIGN(TestPatternReceiver
);
318 class CastStreamingApiTestWithPixelOutput
: public CastStreamingApiTest
{
319 void SetUp() override
{
321 CastStreamingApiTest::SetUp();
324 void SetUpCommandLine(base::CommandLine
* command_line
) override
{
325 command_line
->AppendSwitchASCII(::switches::kWindowSize
, "128,128");
326 CastStreamingApiTest::SetUpCommandLine(command_line
);
330 // Tests the Cast streaming API and its basic functionality end-to-end. An
331 // extension subtest is run to generate test content, capture that content, and
332 // use the API to send it out. At the same time, this test launches an
333 // in-process Cast receiver, listening on a localhost UDP socket, to receive the
334 // content and check whether it matches expectations.
336 // TODO(miu): Now that this test has been long-stable on Release build bots, it
337 // should be enabled for the Debug build bots. http://crbug.com/396413
339 #define MAYBE_EndToEnd EndToEnd
341 #define MAYBE_EndToEnd DISABLED_EndToEnd
343 IN_PROC_BROWSER_TEST_F(CastStreamingApiTestWithPixelOutput
, MAYBE_EndToEnd
) {
344 scoped_ptr
<net::UDPServerSocket
> receive_socket(
345 new net::UDPServerSocket(NULL
, net::NetLog::Source()));
346 receive_socket
->AllowAddressReuse();
347 ASSERT_EQ(net::OK
, receive_socket
->Listen(GetFreeLocalPort()));
348 net::IPEndPoint receiver_end_point
;
349 ASSERT_EQ(net::OK
, receive_socket
->GetLocalAddress(&receiver_end_point
));
350 receive_socket
.reset();
352 // Start the in-process receiver that examines audio/video for the expected
354 const scoped_refptr
<media::cast::StandaloneCastEnvironment
> cast_environment(
355 new media::cast::StandaloneCastEnvironment());
356 TestPatternReceiver
* const receiver
=
357 new TestPatternReceiver(cast_environment
, receiver_end_point
);
359 // Launch the page that: 1) renders the source content; 2) uses the
360 // chrome.tabCapture and chrome.cast.streaming APIs to capture its content and
361 // stream using Cast; and 3) calls chrome.test.succeed() once it is
363 const std::string page_url
= base::StringPrintf(
364 "end_to_end_sender.html?port=%d&aesKey=%s&aesIvMask=%s",
365 receiver_end_point
.port(),
366 base::HexEncode(receiver
->audio_config().aes_key
.data(),
367 receiver
->audio_config().aes_key
.size()).c_str(),
368 base::HexEncode(receiver
->audio_config().aes_iv_mask
.data(),
369 receiver
->audio_config().aes_iv_mask
.size()).c_str());
370 ASSERT_TRUE(RunExtensionSubtest("cast_streaming", page_url
)) << message_
;
372 // Examine the Cast receiver for expected audio/video test patterns. The
373 // colors and tones specified here must match those in end_to_end_sender.js.
374 // Note that we do not check that the color and tone are received
375 // simultaneously since A/V sync should be measured in perf tests.
376 receiver
->AddExpectedTone(200 /* Hz */);
377 receiver
->AddExpectedTone(500 /* Hz */);
378 receiver
->AddExpectedTone(1800 /* Hz */);
379 receiver
->AddExpectedColor(YUVColor(82, 90, 240)); // rgb(255, 0, 0)
380 receiver
->AddExpectedColor(YUVColor(145, 54, 34)); // rgb(0, 255, 0)
381 receiver
->AddExpectedColor(YUVColor(41, 240, 110)); // rgb(0, 0, 255)
383 receiver
->WaitForExpectedTonesAndColors();
387 cast_environment
->Shutdown();
390 IN_PROC_BROWSER_TEST_F(CastStreamingApiTestWithPixelOutput
, RtpStreamError
) {
391 ASSERT_TRUE(RunExtensionSubtest("cast_streaming", "rtp_stream_error.html"));
394 } // namespace extensions