chrome_webrtc_video_quality_browsertest.cc revision 010d83a9304c5a91596085d917d248abff47903a
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/command_line.h"
6#include "base/environment.h"
7#include "base/file_util.h"
8#include "base/path_service.h"
9#include "base/process/launch.h"
10#include "base/strings/string_split.h"
11#include "base/strings/stringprintf.h"
12#include "base/test/test_timeouts.h"
13#include "base/time/time.h"
14#include "chrome/browser/chrome_notification_types.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 "components/infobars/core/infobar.h"
27#include "content/public/browser/notification_service.h"
28#include "content/public/test/browser_test_utils.h"
29#include "media/base/media_switches.h"
30#include "net/test/embedded_test_server/embedded_test_server.h"
31#include "net/test/python_utils.h"
32#include "testing/perf/perf_test.h"
33#include "ui/gl/gl_switches.h"
34
35// For fine-grained suppression on flaky tests.
36#if defined(OS_WIN)
37#include "base/win/windows_version.h"
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    "USERPROFILE";
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 kCapturedYuvFileName[] =
65    FILE_PATH_LITERAL("captured_video.yuv");
66static const base::FilePath::CharType kStatsFileName[] =
67    FILE_PATH_LITERAL("stats.txt");
68static const char kMainWebrtcTestHtmlPage[] =
69    "/webrtc/webrtc_jsep01_test.html";
70
71// If you change the port number, don't forget to modify video_extraction.js
72// too!
73static const char kPyWebSocketPortNumber[] = "12221";
74
75static const struct VideoQualityTestConfig {
76  const char* test_name;
77  int width;
78  int height;
79  const char* capture_page;
80  const base::FilePath::CharType* reference_video;
81  const char* constraints;
82} kVideoConfigurations[] = {
83  { "360p", 640, 360,
84    "/webrtc/webrtc_video_quality_test.html",
85    test::kReferenceFileName360p,
86    WebRtcTestBase::kAudioVideoCallConstraints360p },
87  { "720p", 1280, 720,
88    "/webrtc/webrtc_video_quality_test_hd.html",
89    test::kReferenceFileName720p,
90    WebRtcTestBase::kAudioVideoCallConstraints720p },
91};
92
93// Test the video quality of the WebRTC output.
94//
95// Prerequisites: This test case must run on a machine with a chrome playing
96// the video from the reference files located in GetReferenceFilesDir().
97// The file kReferenceY4mFileName.kY4mFileExtension is played using a
98// FileVideoCaptureDevice and its sibling with kYuvFileExtension is used for
99// comparison.
100//
101// You must also compile the chromium_builder_webrtc target before you run this
102// test to get all the tools built.
103//
104// The external compare_videos.py script also depends on two external
105// executables which must be located in the PATH when running this test.
106// * zxing (see the CPP version at https://code.google.com/p/zxing)
107// * ffmpeg 0.11.1 or compatible version (see http://www.ffmpeg.org)
108//
109// The test runs several custom binaries - rgba_to_i420 converter and
110// frame_analyzer. Both tools can be found under third_party/webrtc/tools. The
111// test also runs a stand alone Python implementation of a WebSocket server
112// (pywebsocket) and a barcode_decoder script.
113class WebRtcVideoQualityBrowserTest : public WebRtcTestBase,
114    public testing::WithParamInterface<VideoQualityTestConfig> {
115 public:
116  WebRtcVideoQualityBrowserTest()
117      : pywebsocket_server_(0),
118        environment_(base::Environment::Create()) {
119    test_config_ = GetParam();
120  }
121
122  virtual void SetUpInProcessBrowserTestFixture() OVERRIDE {
123    DetectErrorsInJavaScript();  // Look for errors in our rather complex js.
124  }
125
126  virtual void SetUpCommandLine(CommandLine* command_line) OVERRIDE {
127    // Set up the command line option with the expected file name. We will check
128    // its existence in HasAllRequiredResources().
129    webrtc_reference_video_y4m_ = test::GetReferenceFilesDir()
130        .Append(test_config_.reference_video)
131        .AddExtension(test::kY4mFileExtension);
132    command_line->AppendSwitchPath(switches::kUseFileForFakeVideoCapture,
133                                   webrtc_reference_video_y4m_);
134    command_line->AppendSwitch(switches::kUseFakeDeviceForMediaStream);
135
136    // The video playback will not work without a GPU, so force its use here.
137    command_line->AppendSwitch(switches::kUseGpuInTests);
138  }
139
140  bool HasAllRequiredResources() {
141    if (!base::PathExists(GetWorkingDir())) {
142      LOG(ERROR) << "Cannot find the working directory for the temporary "
143          "files:" << GetWorkingDir().value();
144      return false;
145    }
146
147    // Ensure we have the required input files.
148    return test::HasReferenceFilesInCheckout();
149  }
150
151  bool StartPyWebSocketServer() {
152    base::FilePath path_pywebsocket_dir =
153        GetSourceDir().Append(FILE_PATH_LITERAL("third_party/pywebsocket/src"));
154    base::FilePath pywebsocket_server = path_pywebsocket_dir.Append(
155        FILE_PATH_LITERAL("mod_pywebsocket/standalone.py"));
156    base::FilePath path_to_data_handler =
157        GetSourceDir().Append(FILE_PATH_LITERAL("chrome/test/data/webrtc/wsh"));
158
159    if (!base::PathExists(pywebsocket_server)) {
160      LOG(ERROR) << "Missing pywebsocket server.";
161      return false;
162    }
163    if (!base::PathExists(path_to_data_handler)) {
164      LOG(ERROR) << "Missing data handler for pywebsocket server.";
165      return false;
166    }
167
168    AppendToPythonPath(path_pywebsocket_dir);
169
170    // Note: don't append switches to this command since it will mess up the
171    // -u in the python invocation!
172    CommandLine pywebsocket_command(CommandLine::NO_PROGRAM);
173    EXPECT_TRUE(GetPythonCommand(&pywebsocket_command));
174
175    pywebsocket_command.AppendArgPath(pywebsocket_server);
176    pywebsocket_command.AppendArg("-p");
177    pywebsocket_command.AppendArg(kPyWebSocketPortNumber);
178    pywebsocket_command.AppendArg("-d");
179    pywebsocket_command.AppendArgPath(path_to_data_handler);
180
181    VLOG(0) << "Running " << pywebsocket_command.GetCommandLineString();
182    return base::LaunchProcess(pywebsocket_command, base::LaunchOptions(),
183                               &pywebsocket_server_);
184  }
185
186  bool ShutdownPyWebSocketServer() {
187    return base::KillProcess(pywebsocket_server_, 0, false);
188  }
189
190  // Runs the RGBA to I420 converter on the video in |capture_video_filename|,
191  // which should contain frames of size |width| x |height|.
192  //
193  // The rgba_to_i420_converter is part of the webrtc_test_tools target which
194  // should be build prior to running this test. The resulting binary should
195  // live next to Chrome.
196  bool RunARGBtoI420Converter(int width,
197                              int height,
198                              const base::FilePath& captured_video_filename) {
199    base::FilePath path_to_converter = base::MakeAbsoluteFilePath(
200        GetBrowserDir().Append(kArgbToI420ConverterExecutable));
201
202    if (!base::PathExists(path_to_converter)) {
203      LOG(ERROR) << "Missing ARGB->I420 converter: should be in "
204          << path_to_converter.value();
205      return false;
206    }
207
208    CommandLine converter_command(path_to_converter);
209    converter_command.AppendSwitchPath("--frames_dir", GetWorkingDir());
210    converter_command.AppendSwitchPath("--output_file",
211                                       captured_video_filename);
212    converter_command.AppendSwitchASCII("--width",
213                                        base::StringPrintf("%d", width));
214    converter_command.AppendSwitchASCII("--height",
215                                        base::StringPrintf("%d", height));
216    converter_command.AppendSwitchASCII("--delete_frames", "true");
217
218    // We produce an output file that will later be used as an input to the
219    // barcode decoder and frame analyzer tools.
220    VLOG(0) << "Running " << converter_command.GetCommandLineString();
221    std::string result;
222    bool ok = base::GetAppOutput(converter_command, &result);
223    VLOG(0) << "Output was:\n\n" << result;
224    return ok;
225  }
226
227  // Compares the |captured_video_filename| with the |reference_video_filename|.
228  //
229  // The barcode decoder decodes the captured video containing barcodes overlaid
230  // into every frame of the video (produced by rgba_to_i420_converter). It
231  // produces a set of PNG images and a |stats_file| that maps each captured
232  // frame to a frame in the reference video. The frames should be of size
233  // |width| x |height|.
234  // All measurements calculated are printed as perf parsable numbers to stdout.
235  bool CompareVideosAndPrintResult(
236      const char* test_label,
237      int width,
238      int height,
239      const base::FilePath& captured_video_filename,
240      const base::FilePath& reference_video_filename,
241      const base::FilePath& stats_file) {
242
243    base::FilePath path_to_analyzer = base::MakeAbsoluteFilePath(
244        GetBrowserDir().Append(kFrameAnalyzerExecutable));
245    base::FilePath path_to_compare_script = GetSourceDir().Append(
246        FILE_PATH_LITERAL("third_party/webrtc/tools/compare_videos.py"));
247
248    if (!base::PathExists(path_to_analyzer)) {
249      LOG(ERROR) << "Missing frame analyzer: should be in "
250          << path_to_analyzer.value();
251      return false;
252    }
253    if (!base::PathExists(path_to_compare_script)) {
254      LOG(ERROR) << "Missing video compare script: should be in "
255          << path_to_compare_script.value();
256      return false;
257    }
258
259    // Note: don't append switches to this command since it will mess up the
260    // -u in the python invocation!
261    CommandLine compare_command(CommandLine::NO_PROGRAM);
262    EXPECT_TRUE(GetPythonCommand(&compare_command));
263
264    compare_command.AppendArgPath(path_to_compare_script);
265    compare_command.AppendArg(base::StringPrintf("--label=%s", test_label));
266    compare_command.AppendArg("--ref_video");
267    compare_command.AppendArgPath(reference_video_filename);
268    compare_command.AppendArg("--test_video");
269    compare_command.AppendArgPath(captured_video_filename);
270    compare_command.AppendArg("--frame_analyzer");
271    compare_command.AppendArgPath(path_to_analyzer);
272    compare_command.AppendArg("--yuv_frame_width");
273    compare_command.AppendArg(base::StringPrintf("%d", width));
274    compare_command.AppendArg("--yuv_frame_height");
275    compare_command.AppendArg(base::StringPrintf("%d", height));
276    compare_command.AppendArg("--stats_file");
277    compare_command.AppendArgPath(stats_file);
278
279    VLOG(0) << "Running " << compare_command.GetCommandLineString();
280    std::string output;
281    bool ok = base::GetAppOutput(compare_command, &output);
282    // Print to stdout to ensure the perf numbers are parsed properly by the
283    // buildbot step.
284    printf("Output was:\n\n%s\n", output.c_str());
285    return ok;
286  }
287
288  base::FilePath GetWorkingDir() {
289    std::string home_dir;
290    environment_->GetVar(kHomeEnvName, &home_dir);
291    base::FilePath::StringType native_home_dir(home_dir.begin(),
292                                               home_dir.end());
293    return base::FilePath(native_home_dir).Append(kWorkingDirName);
294  }
295
296 protected:
297  VideoQualityTestConfig test_config_;
298
299 private:
300  base::FilePath GetSourceDir() {
301    base::FilePath source_dir;
302    PathService::Get(base::DIR_SOURCE_ROOT, &source_dir);
303    return source_dir;
304  }
305
306  base::FilePath GetBrowserDir() {
307    base::FilePath browser_dir;
308    EXPECT_TRUE(PathService::Get(base::DIR_MODULE, &browser_dir));
309    return browser_dir;
310  }
311
312  base::ProcessHandle pywebsocket_server_;
313  scoped_ptr<base::Environment> environment_;
314  base::FilePath webrtc_reference_video_y4m_;
315};
316
317INSTANTIATE_TEST_CASE_P(
318    WebRtcVideoQualityBrowserTests,
319    WebRtcVideoQualityBrowserTest,
320    testing::ValuesIn(kVideoConfigurations));
321
322IN_PROC_BROWSER_TEST_P(WebRtcVideoQualityBrowserTest,
323                       MANUAL_TestVideoQuality) {
324
325#if defined(OS_WIN)
326  // Fails on XP. http://crbug.com/353078
327  if (base::win::GetVersion() <= base::win::VERSION_XP)
328    return;
329#endif
330
331  ASSERT_GE(TestTimeouts::action_max_timeout().InSeconds(), 150) <<
332      "This is a long-running test; you must specify "
333      "--ui-test-action-max-timeout to have a value of at least 150000.";
334  ASSERT_TRUE(HasAllRequiredResources());
335  ASSERT_TRUE(embedded_test_server()->InitializeAndWaitUntilReady());
336  ASSERT_TRUE(StartPyWebSocketServer());
337
338  content::WebContents* left_tab =
339      OpenPageAndGetUserMediaInNewTabWithConstraints(
340          embedded_test_server()->GetURL(kMainWebrtcTestHtmlPage),
341          test_config_.constraints);
342  content::WebContents* right_tab =
343      OpenPageAndGetUserMediaInNewTabWithConstraints(
344          embedded_test_server()->GetURL(test_config_.capture_page),
345          test_config_.constraints);
346
347  SetupPeerconnectionWithLocalStream(left_tab);
348  SetupPeerconnectionWithLocalStream(right_tab);
349
350  NegotiateCall(left_tab, right_tab);
351
352  // Poll slower here to avoid flooding the log with messages: capturing and
353  // sending frames take quite a bit of time.
354  int polling_interval_msec = 1000;
355
356  EXPECT_TRUE(test::PollingWaitUntil(
357      "doneFrameCapturing()", "done-capturing", right_tab,
358      polling_interval_msec));
359
360  HangUp(left_tab);
361
362  EXPECT_TRUE(test::PollingWaitUntil(
363      "haveMoreFramesToSend()", "no-more-frames", right_tab,
364      polling_interval_msec));
365
366  // Shut everything down to avoid having the javascript race with the analysis
367  // tools. For instance, dont have console log printouts interleave with the
368  // RESULT lines from the analysis tools (crbug.com/323200).
369  ASSERT_TRUE(ShutdownPyWebSocketServer());
370
371  chrome::CloseWebContents(browser(), left_tab, false);
372  chrome::CloseWebContents(browser(), right_tab, false);
373
374  RunARGBtoI420Converter(
375      test_config_.width, test_config_.height,
376      GetWorkingDir().Append(kCapturedYuvFileName));
377  ASSERT_TRUE(CompareVideosAndPrintResult(
378      test_config_.test_name,
379      test_config_.width,
380      test_config_.height,
381      GetWorkingDir().Append(kCapturedYuvFileName),
382      test::GetReferenceFilesDir()
383          .Append(test_config_.reference_video)
384          .AddExtension(test::kYuvFileExtension),
385      GetWorkingDir().Append(kStatsFileName)));
386}
387