chrome_webrtc_video_quality_browsertest.cc revision 558790d6acca3451cf3a6b497803a5f07d0bec58
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 "base/environment.h"
6#include "base/file_util.h"
7#include "base/path_service.h"
8#include "base/process/launch.h"
9#include "base/strings/string_split.h"
10#include "base/strings/stringprintf.h"
11#include "base/test/test_timeouts.h"
12#include "base/time/time.h"
13#include "chrome/browser/chrome_notification_types.h"
14#include "chrome/browser/infobars/infobar.h"
15#include "chrome/browser/infobars/infobar_service.h"
16#include "chrome/browser/media/media_stream_infobar_delegate.h"
17#include "chrome/browser/media/webrtc_browsertest_base.h"
18#include "chrome/browser/media/webrtc_browsertest_common.h"
19#include "chrome/browser/profiles/profile.h"
20#include "chrome/browser/ui/browser.h"
21#include "chrome/browser/ui/browser_tabstrip.h"
22#include "chrome/browser/ui/tabs/tab_strip_model.h"
23#include "chrome/common/chrome_switches.h"
24#include "chrome/test/base/in_process_browser_test.h"
25#include "chrome/test/base/ui_test_utils.h"
26#include "chrome/test/perf/perf_test.h"
27#include "chrome/test/ui/ui_test.h"
28#include "content/public/browser/notification_service.h"
29#include "content/public/test/browser_test_utils.h"
30#include "net/test/python_utils.h"
31#include "net/test/spawned_test_server/spawned_test_server.h"
32
33static const base::FilePath::CharType kPeerConnectionServer[] =
34#if defined(OS_WIN)
35    FILE_PATH_LITERAL("peerconnection_server.exe");
36#else
37    FILE_PATH_LITERAL("peerconnection_server");
38#endif
39
40static const base::FilePath::CharType kFrameAnalyzerExecutable[] =
41#if defined(OS_WIN)
42    FILE_PATH_LITERAL("frame_analyzer.exe");
43#else
44    FILE_PATH_LITERAL("frame_analyzer");
45#endif
46
47static const base::FilePath::CharType kArgbToI420ConverterExecutable[] =
48#if defined(OS_WIN)
49    FILE_PATH_LITERAL("rgba_to_i420_converter.exe");
50#else
51    FILE_PATH_LITERAL("rgba_to_i420_converter");
52#endif
53
54static const char kHomeEnvName[] =
55#if defined(OS_WIN)
56    "HOMEPATH";
57#else
58    "HOME";
59#endif
60
61// The working dir should be in the user's home folder.
62static const base::FilePath::CharType kWorkingDirName[] =
63    FILE_PATH_LITERAL("webrtc_video_quality");
64static const base::FilePath::CharType kReferenceYuvFileName[] =
65    FILE_PATH_LITERAL("reference_video.yuv");
66static const base::FilePath::CharType kCapturedYuvFileName[] =
67    FILE_PATH_LITERAL("captured_video.yuv");
68static const base::FilePath::CharType kStatsFileName[] =
69    FILE_PATH_LITERAL("stats.txt");
70static const char kMainWebrtcTestHtmlPage[] =
71    "files/webrtc/webrtc_jsep01_test.html";
72static const char kCapturingWebrtcHtmlPage[] =
73    "files/webrtc/webrtc_video_quality_test.html";
74static const int kVgaWidth = 640;
75static const int kVgaHeight = 480;
76
77// If you change the port number, don't forget to modify video_extraction.js
78// too!
79static const char kPyWebSocketPortNumber[] = "12221";
80
81// Test the video quality of the WebRTC output.
82//
83// Prerequisites: This test case must run on a machine with a virtual webcam
84// that plays video from the reference file located in <the running users home
85// folder>/kWorkingDirName/kReferenceYuvFileName.
86//
87// You must also compile the chromium_builder_webrtc target before you run this
88// test to get all the tools built.
89//
90// The external compare_videos.py script also depends on two external
91// executables which must be located in the PATH when running this test.
92// * zxing (see the CPP version at https://code.google.com/p/zxing)
93// * ffmpeg 0.11.1 or compatible version (see http://www.ffmpeg.org)
94//
95// The test case will launch a custom binary (peerconnection_server) which will
96// allow two WebRTC clients to find each other.
97//
98// The test also runs several other custom binaries - rgba_to_i420 converter and
99// frame_analyzer. Both tools can be found under third_party/webrtc/tools. The
100// test also runs a stand alone Python implementation of a WebSocket server
101// (pywebsocket) and a barcode_decoder script.
102class WebrtcVideoQualityBrowserTest : public WebRtcTestBase {
103 public:
104  WebrtcVideoQualityBrowserTest()
105      : peerconnection_server_(0),
106        pywebsocket_server_(0),
107        environment_(base::Environment::Create()) {}
108
109  virtual void SetUp() OVERRIDE {
110    RunPeerConnectionServer();
111    InProcessBrowserTest::SetUp();
112
113    // Ensure we have the stuff we need.
114    EXPECT_TRUE(base::PathExists(GetWorkingDir()))
115        << "Cannot find the working directory for the reference video and "
116           "the temporary files:" << GetWorkingDir().value();
117    base::FilePath reference_file =
118        GetWorkingDir().Append(kReferenceYuvFileName);
119    EXPECT_TRUE(base::PathExists(reference_file))
120        << "Cannot find the reference file to be used for video quality "
121        << "comparison: " << reference_file.value();
122  }
123
124  virtual void TearDown() OVERRIDE {
125    ShutdownPeerConnectionServer();
126    InProcessBrowserTest::TearDown();
127    ShutdownPyWebSocketServer();
128  }
129
130  virtual void SetUpCommandLine(CommandLine* command_line) OVERRIDE {
131    // TODO(phoglund): check that user actually has the requisite devices and
132    // print a nice message if not; otherwise the test just times out which can
133    // be confusing.
134    // This test expects real device handling and requires a real webcam / audio
135    // device; it will not work with fake devices.
136    EXPECT_FALSE(
137        command_line->HasSwitch(switches::kUseFakeDeviceForMediaStream))
138        << "You cannot run this test with fake devices.";
139  }
140
141  void StartPyWebSocketServer() {
142    base::FilePath path_pywebsocket_dir =
143        GetSourceDir().Append(FILE_PATH_LITERAL("third_party/pywebsocket/src"));
144    base::FilePath pywebsocket_server = path_pywebsocket_dir.Append(
145        FILE_PATH_LITERAL("mod_pywebsocket/standalone.py"));
146    base::FilePath path_to_data_handler =
147        GetSourceDir().Append(FILE_PATH_LITERAL("chrome/test/functional"));
148
149    EXPECT_TRUE(base::PathExists(pywebsocket_server))
150        << "Fatal: missing pywebsocket server.";
151    EXPECT_TRUE(base::PathExists(path_to_data_handler))
152        << "Fatal: missing data handler for pywebsocket server.";
153
154    AppendToPythonPath(path_pywebsocket_dir);
155    CommandLine pywebsocket_command = MakePythonCommand(pywebsocket_server);
156
157    // Construct the command line manually, the server doesn't support -arg=val.
158    pywebsocket_command.AppendArg("-p");
159    pywebsocket_command.AppendArg(kPyWebSocketPortNumber);
160    pywebsocket_command.AppendArg("-d");
161    pywebsocket_command.AppendArgPath(path_to_data_handler);
162
163    LOG(INFO) << "Running " << pywebsocket_command.GetCommandLineString();
164    EXPECT_TRUE(base::LaunchProcess(
165        pywebsocket_command, base::LaunchOptions(), &pywebsocket_server_))
166        << "Failed to launch pywebsocket server.";
167  }
168
169  void ShutdownPyWebSocketServer() {
170    EXPECT_TRUE(base::KillProcess(pywebsocket_server_, 0, false))
171        << "Failed to shut down pywebsocket server!";
172  }
173
174  // Convenience method which executes the provided javascript in the context
175  // of the provided web contents and returns what it evaluated to.
176  std::string ExecuteJavascript(const std::string& javascript,
177                                content::WebContents* tab_contents) {
178    std::string result;
179    EXPECT_TRUE(content::ExecuteScriptAndExtractString(
180        tab_contents, javascript, &result));
181    return result;
182  }
183
184  // Ensures we didn't get any errors asynchronously (e.g. while no javascript
185  // call from this test was outstanding).
186  // TODO(phoglund): this becomes obsolete when we switch to communicating with
187  // the DOM message queue.
188  void AssertNoAsynchronousErrors(content::WebContents* tab_contents) {
189    EXPECT_EQ("ok-no-errors",
190              ExecuteJavascript("getAnyTestFailures()", tab_contents));
191  }
192
193  // The peer connection server lets our two tabs find each other and talk to
194  // each other (e.g. it is the application-specific "signaling solution").
195  void ConnectToPeerConnectionServer(const std::string peer_name,
196                                     content::WebContents* tab_contents) {
197    std::string javascript = base::StringPrintf(
198        "connect('http://localhost:8888', '%s');", peer_name.c_str());
199    EXPECT_EQ("ok-connected", ExecuteJavascript(javascript, tab_contents));
200  }
201
202  void EstablishCall(content::WebContents* from_tab,
203                     content::WebContents* to_tab) {
204    EXPECT_EQ("ok-peerconnection-created",
205              ExecuteJavascript("preparePeerConnection()", from_tab));
206    EXPECT_EQ("ok-added", ExecuteJavascript("addLocalStream()", from_tab));
207    EXPECT_EQ("ok-negotiating", ExecuteJavascript("negotiateCall()", from_tab));
208
209    // Ensure the call gets up on both sides.
210    EXPECT_TRUE(PollingWaitUntil(
211        "getPeerConnectionReadyState()", "active", from_tab));
212    EXPECT_TRUE(PollingWaitUntil(
213        "getPeerConnectionReadyState()", "active", to_tab));
214  }
215
216  void HangUp(content::WebContents* from_tab) {
217    EXPECT_EQ("ok-call-hung-up", ExecuteJavascript("hangUp()", from_tab));
218  }
219
220  void WaitUntilHangupVerified(content::WebContents* tab_contents) {
221    EXPECT_TRUE(PollingWaitUntil(
222        "getPeerConnectionReadyState()", "no-peer-connection", tab_contents));
223  }
224
225  // Runs the RGBA to I420 converter on the video in |capture_video_filename|,
226  // which should contain frames of size |width| x |height|.
227  //
228  // The rgba_to_i420_converter is part of the webrtc_test_tools target which
229  // should be build prior to running this test. The resulting binary should
230  // live next to Chrome.
231  void RunARGBtoI420Converter(int width,
232                              int height,
233                              const base::FilePath& captured_video_filename) {
234    base::FilePath path_to_converter = base::MakeAbsoluteFilePath(
235        GetBrowserDir().Append(kArgbToI420ConverterExecutable));
236    EXPECT_TRUE(base::PathExists(path_to_converter))
237        << "Missing ARGB->I420 converter: should be in "
238        << path_to_converter.value();
239
240    CommandLine converter_command(path_to_converter);
241    converter_command.AppendSwitchPath("--frames_dir", GetWorkingDir());
242    converter_command.AppendSwitchPath("--output_file",
243                                       captured_video_filename);
244    converter_command.AppendSwitchASCII("--width",
245                                        base::StringPrintf("%d", width));
246    converter_command.AppendSwitchASCII("--height",
247                                        base::StringPrintf("%d", height));
248
249    // We produce an output file that will later be used as an input to the
250    // barcode decoder and frame analyzer tools.
251    LOG(INFO) << "Running " << converter_command.GetCommandLineString();
252    std::string result;
253    EXPECT_TRUE(base::GetAppOutput(converter_command, &result));
254    LOG(INFO) << "Output was:\n\n" << result;
255  }
256
257  // Compares the |captured_video_filename| with the |reference_video_filename|.
258  //
259  // The barcode decoder decodes the captured video containing barcodes overlaid
260  // into every frame of the video (produced by rgba_to_i420_converter). It
261  // produces a set of PNG images and a |stats_file| that maps each captured
262  // frame to a frame in the reference video. The frames should be of size
263  // |width| x |height|. The output of compare_videos.py is returned.
264  std::string CompareVideos(int width,
265                            int height,
266                            const base::FilePath& captured_video_filename,
267                            const base::FilePath& reference_video_filename,
268                            const base::FilePath& stats_file) {
269    base::FilePath path_to_analyzer = base::MakeAbsoluteFilePath(
270        GetBrowserDir().Append(kFrameAnalyzerExecutable));
271    base::FilePath path_to_compare_script = GetSourceDir().Append(
272        FILE_PATH_LITERAL("third_party/webrtc/tools/compare_videos.py"));
273
274    EXPECT_TRUE(base::PathExists(path_to_analyzer))
275        << "Missing frame analyzer: should be in " << path_to_analyzer.value();
276    EXPECT_TRUE(base::PathExists(path_to_compare_script))
277        << "Missing video compare script: should be in "
278        << path_to_compare_script.value();
279
280    CommandLine compare_command = MakePythonCommand(path_to_compare_script);
281    compare_command.AppendSwitchPath("--ref_video", reference_video_filename);
282    compare_command.AppendSwitchPath("--test_video", captured_video_filename);
283    compare_command.AppendSwitchPath("--frame_analyzer", path_to_analyzer);
284    compare_command.AppendSwitchASCII("--yuv_frame_width",
285                                      base::StringPrintf("%d", width));
286    compare_command.AppendSwitchASCII("--yuv_frame_height",
287                                      base::StringPrintf("%d", height));
288    compare_command.AppendSwitchPath("--stats_file", stats_file);
289
290    LOG(INFO) << "Running " << compare_command.GetCommandLineString();
291    std::string result;
292    EXPECT_TRUE(base::GetAppOutput(compare_command, &result));
293    LOG(INFO) << "Output was:\n\n" << result;
294    return result;
295  }
296
297  // Processes the |frame_analyzer_output| for the different frame counts.
298  //
299  // The frame analyzer outputs additional information about the number of
300  // unique frames captured, The max number of repeated frames in a sequence and
301  // the max number of skipped frames. These values are then written to the Perf
302  // Graph. (Note: Some of the repeated or skipped frames will probably be due
303  // to the imperfection of JavaScript timers).
304  void PrintFramesCountPerfResults(std::string frame_analyzer_output) {
305    size_t unique_frames_pos =
306        frame_analyzer_output.rfind("Unique_frames_count");
307    EXPECT_NE(unique_frames_pos, std::string::npos)
308        << "Missing Unique_frames_count in frame analyzer output:\n"
309        << frame_analyzer_output;
310
311    std::string unique_frame_counts =
312        frame_analyzer_output.substr(unique_frames_pos);
313    // TODO(phoglund): Fix ESTATS result to not have this silly newline.
314    std::replace(
315        unique_frame_counts.begin(), unique_frame_counts.end(), '\n', ' ');
316
317    std::vector<std::pair<std::string, std::string> > key_values;
318    base::SplitStringIntoKeyValuePairs(
319        unique_frame_counts, ':', ' ', &key_values);
320    std::vector<std::pair<std::string, std::string> >::const_iterator iter;
321    for (iter = key_values.begin(); iter != key_values.end(); ++iter) {
322      const std::pair<std::string, std::string>& key_value = *iter;
323      perf_test::PrintResult(
324          key_value.first, "", "VGA", key_value.second, "", false);
325    }
326  }
327
328  // Processes the |frame_analyzer_output| to extract the PSNR and SSIM values.
329  //
330  // The frame analyzer produces PSNR and SSIM results for every unique frame
331  // that has been captured. This method forms a list of all the psnr and ssim
332  // values and passes it to PrintResultList() for printing on the Perf Graph.
333  void PrintPsnrAndSsimPerfResults(std::string frame_analyzer_output) {
334    size_t stats_start = frame_analyzer_output.find("BSTATS");
335    EXPECT_NE(stats_start, std::string::npos)
336        << "Missing BSTATS in frame analyzer output:\n"
337        << frame_analyzer_output;
338    size_t stats_end = frame_analyzer_output.find("ESTATS");
339    EXPECT_NE(stats_end, std::string::npos)
340        << "Missing ESTATS in frame analyzer output:\n"
341        << frame_analyzer_output;
342
343    stats_start += std::string("BSTATS").size();
344    std::string psnr_ssim_stats =
345        frame_analyzer_output.substr(stats_start, stats_end - stats_start);
346
347    // PSNR and SSIM values aren't really key-value pairs but it is convenient
348    // to parse them as such.
349    // TODO(phoglund): make the format more convenient so we need less
350    // processing here.
351    std::vector<std::pair<std::string, std::string> > psnr_ssim_entries;
352    base::SplitStringIntoKeyValuePairs(
353        psnr_ssim_stats, ' ', ';', &psnr_ssim_entries);
354
355    std::string psnr_value_list;
356    std::string ssim_value_list;
357    std::vector<std::pair<std::string, std::string> >::const_iterator iter;
358    for (iter = psnr_ssim_entries.begin(); iter != psnr_ssim_entries.end();
359         ++iter) {
360      const std::pair<std::string, std::string>& psnr_and_ssim = *iter;
361      psnr_value_list.append(psnr_and_ssim.first).append(",");
362      ssim_value_list.append(psnr_and_ssim.second).append(",");
363    }
364    // Nuke last comma.
365    psnr_value_list.erase(psnr_value_list.size() - 1);
366    ssim_value_list.erase(ssim_value_list.size() - 1);
367
368    perf_test::PrintResultList("PSNR", "", "VGA", psnr_value_list, "dB", false);
369    perf_test::PrintResultList("SSIM", "", "VGA", ssim_value_list, "", false);
370  }
371
372  base::FilePath GetWorkingDir() {
373    std::string home_dir;
374    environment_->GetVar(kHomeEnvName, &home_dir);
375    base::FilePath::StringType native_home_dir(home_dir.begin(),
376                                               home_dir.end());
377    return base::FilePath(native_home_dir).Append(kWorkingDirName);
378  }
379
380 private:
381  void RunPeerConnectionServer() {
382    // TODO(phoglund): de-dupe later: next line differs from original.
383    base::FilePath peerconnection_server =
384        GetBrowserDir().Append(kPeerConnectionServer);
385
386    EXPECT_TRUE(base::PathExists(peerconnection_server))
387        << "Missing peerconnection_server. You must build "
388           "it so it ends up next to the browser test binary.";
389    EXPECT_TRUE(base::LaunchProcess(CommandLine(peerconnection_server),
390                                    base::LaunchOptions(),
391                                    &peerconnection_server_))
392        << "Failed to launch peerconnection_server.";
393  }
394
395  void ShutdownPeerConnectionServer() {
396    EXPECT_TRUE(base::KillProcess(peerconnection_server_, 0, false))
397        << "Failed to shut down peerconnection_server!";
398  }
399
400  base::FilePath GetSourceDir() {
401    base::FilePath source_dir;
402    PathService::Get(base::DIR_SOURCE_ROOT, &source_dir);
403    return source_dir;
404  }
405
406  base::FilePath GetBrowserDir() {
407    base::FilePath browser_dir;
408    EXPECT_TRUE(PathService::Get(base::DIR_MODULE, &browser_dir));
409    return browser_dir;
410  }
411
412  CommandLine MakePythonCommand(base::FilePath python_script) {
413    CommandLine python_command(CommandLine::NO_PROGRAM);
414    EXPECT_TRUE(GetPythonCommand(&python_command));
415    CommandLine complete_command(python_script);
416    complete_command.PrependWrapper(python_command.GetCommandLineString());
417    return complete_command;
418  }
419
420  base::ProcessHandle peerconnection_server_;
421  base::ProcessHandle pywebsocket_server_;
422  scoped_ptr<base::Environment> environment_;
423};
424
425#if defined(OS_WIN)
426// Broken on Win: failing to start pywebsocket_server. http://crbug.com/255499.
427#define MAYBE_MANUAL_TestVGAVideoQuality DISABLED_MANUAL_TestVGAVideoQuality
428#else
429#define MAYBE_MANUAL_TestVGAVideoQuality MANUAL_TestVGAVideoQuality
430#endif
431
432IN_PROC_BROWSER_TEST_F(WebrtcVideoQualityBrowserTest,
433                       MAYBE_MANUAL_TestVGAVideoQuality) {
434  // TODO(phoglund): de-dupe from chrome_webrtc_browsertest.cc.
435  StartPyWebSocketServer();
436
437  EXPECT_TRUE(test_server()->Start());
438
439  ui_test_utils::NavigateToURL(browser(),
440                               test_server()->GetURL(kMainWebrtcTestHtmlPage));
441  content::WebContents* left_tab =
442      browser()->tab_strip_model()->GetActiveWebContents();
443  GetUserMediaAndAccept(left_tab);
444
445  chrome::AddBlankTabAt(browser(), -1, true);
446  content::WebContents* right_tab =
447      browser()->tab_strip_model()->GetActiveWebContents();
448  // TODO(phoglund): (de-dupe later) different from original flow.
449  ui_test_utils::NavigateToURL(browser(),
450                               test_server()->GetURL(kCapturingWebrtcHtmlPage));
451  GetUserMediaAndAccept(right_tab);
452
453  ConnectToPeerConnectionServer("peer 1", left_tab);
454  ConnectToPeerConnectionServer("peer 2", right_tab);
455
456  EstablishCall(left_tab, right_tab);
457
458  AssertNoAsynchronousErrors(left_tab);
459  AssertNoAsynchronousErrors(right_tab);
460
461  // Poll slower here to avoid flooding the log with messages: capturing and
462  // sending frames take quite a bit of time.
463  int polling_interval_msec = 1000;
464
465  // TODO(phoglund): (de-dupe later) different from original flow.
466  EXPECT_TRUE(PollingWaitUntil(
467      "doneFrameCapturing()", "done-capturing", right_tab,
468      polling_interval_msec));
469
470  HangUp(left_tab);
471  WaitUntilHangupVerified(left_tab);
472  WaitUntilHangupVerified(right_tab);
473
474  AssertNoAsynchronousErrors(left_tab);
475  AssertNoAsynchronousErrors(right_tab);
476
477  // TODO(phoglund): (de-dupe later) different from original flow.
478  EXPECT_TRUE(PollingWaitUntil(
479      "haveMoreFramesToSend()", "no-more-frames", right_tab,
480      polling_interval_msec));
481
482  RunARGBtoI420Converter(
483      kVgaWidth, kVgaHeight, GetWorkingDir().Append(kCapturedYuvFileName));
484  std::string output =
485      CompareVideos(kVgaWidth,
486                    kVgaHeight,
487                    GetWorkingDir().Append(kCapturedYuvFileName),
488                    GetWorkingDir().Append(kReferenceYuvFileName),
489                    GetWorkingDir().Append(kStatsFileName));
490
491  PrintFramesCountPerfResults(output);
492  PrintPsnrAndSsimPerfResults(output);
493}
494