chrome_webrtc_video_quality_browsertest.cc revision 5d1f7b1de12d16ceb2c938c56701a3e8bfa558f7
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/ui/ui_test.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 35static const base::FilePath::CharType kFrameAnalyzerExecutable[] = 36#if defined(OS_WIN) 37 FILE_PATH_LITERAL("frame_analyzer.exe"); 38#else 39 FILE_PATH_LITERAL("frame_analyzer"); 40#endif 41 42static const base::FilePath::CharType kArgbToI420ConverterExecutable[] = 43#if defined(OS_WIN) 44 FILE_PATH_LITERAL("rgba_to_i420_converter.exe"); 45#else 46 FILE_PATH_LITERAL("rgba_to_i420_converter"); 47#endif 48 49static const char kHomeEnvName[] = 50#if defined(OS_WIN) 51 "HOMEPATH"; 52#else 53 "HOME"; 54#endif 55 56// The working dir should be in the user's home folder. 57static const base::FilePath::CharType kWorkingDirName[] = 58 FILE_PATH_LITERAL("webrtc_video_quality"); 59static const base::FilePath::CharType kReferenceYuvFileName[] = 60 FILE_PATH_LITERAL("reference_video.yuv"); 61static const base::FilePath::CharType kCapturedYuvFileName[] = 62 FILE_PATH_LITERAL("captured_video.yuv"); 63static const base::FilePath::CharType kStatsFileName[] = 64 FILE_PATH_LITERAL("stats.txt"); 65static const char kMainWebrtcTestHtmlPage[] = 66 "/webrtc/webrtc_jsep01_test.html"; 67static const char kCapturingWebrtcHtmlPage[] = 68 "/webrtc/webrtc_video_quality_test.html"; 69static const int kVgaWidth = 640; 70static const int kVgaHeight = 480; 71 72// If you change the port number, don't forget to modify video_extraction.js 73// too! 74static const char kPyWebSocketPortNumber[] = "12221"; 75 76// Test the video quality of the WebRTC output. 77// 78// Prerequisites: This test case must run on a machine with a virtual webcam 79// that plays video from the reference file located in <the running user's home 80// folder>/kWorkingDirName/kReferenceYuvFileName. 81// 82// You must also compile the chromium_builder_webrtc target before you run this 83// test to get all the tools built. 84// 85// The external compare_videos.py script also depends on two external 86// executables which must be located in the PATH when running this test. 87// * zxing (see the CPP version at https://code.google.com/p/zxing) 88// * ffmpeg 0.11.1 or compatible version (see http://www.ffmpeg.org) 89// 90// The test case will launch a custom binary (peerconnection_server) which will 91// allow two WebRTC clients to find each other. 92// 93// The test also runs several other custom binaries - rgba_to_i420 converter and 94// frame_analyzer. Both tools can be found under third_party/webrtc/tools. The 95// test also runs a stand alone Python implementation of a WebSocket server 96// (pywebsocket) and a barcode_decoder script. 97class WebRtcVideoQualityBrowserTest : public WebRtcTestBase { 98 public: 99 WebRtcVideoQualityBrowserTest() 100 : pywebsocket_server_(0), 101 environment_(base::Environment::Create()) {} 102 103 virtual void SetUpInProcessBrowserTestFixture() OVERRIDE { 104 PeerConnectionServerRunner::KillAllPeerConnectionServersOnCurrentSystem(); 105 DetectErrorsInJavaScript(); // Look for errors in our rather complex js. 106 } 107 108 virtual void SetUpCommandLine(CommandLine* command_line) OVERRIDE { 109 // This test expects real device handling and requires a real webcam / audio 110 // device; it will not work with fake devices. 111 EXPECT_FALSE( 112 command_line->HasSwitch(switches::kUseFakeDeviceForMediaStream)) 113 << "You cannot run this test with fake devices."; 114 115#if defined(OS_MACOSX) 116 // TODO(mcasas): Remove this switch when ManyCam virtual video capture 117 // device starts supporting AVFoundation, see http://crbug.com/327618. 118 command_line->AppendSwitch(switches::kDisableAVFoundation); 119#endif 120 121 // The video playback will not work without a GPU, so force its use here. 122 command_line->AppendSwitch(switches::kUseGpuInTests); 123 } 124 125 bool HasAllRequiredResources() { 126 if (!base::PathExists(GetWorkingDir())) { 127 LOG(ERROR) << "Cannot find the working directory for the reference video " 128 "and the temporary files:" << GetWorkingDir().value(); 129 return false; 130 } 131 base::FilePath reference_file = 132 GetWorkingDir().Append(kReferenceYuvFileName); 133 if (!base::PathExists(reference_file)) { 134 LOG(ERROR) << "Cannot find the reference file to be used for video " 135 << "quality comparison: " << reference_file.value(); 136 return false; 137 } 138 return true; 139 } 140 141 bool 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 if (!base::PathExists(pywebsocket_server)) { 150 LOG(ERROR) << "Missing pywebsocket server."; 151 return false; 152 } 153 if (!base::PathExists(path_to_data_handler)) { 154 LOG(ERROR) << "Missing data handler for pywebsocket server."; 155 return false; 156 } 157 158 AppendToPythonPath(path_pywebsocket_dir); 159 160 // Note: don't append switches to this command since it will mess up the 161 // -u in the python invocation! 162 CommandLine pywebsocket_command(CommandLine::NO_PROGRAM); 163 EXPECT_TRUE(GetPythonCommand(&pywebsocket_command)); 164 165 pywebsocket_command.AppendArgPath(pywebsocket_server); 166 pywebsocket_command.AppendArg("-p"); 167 pywebsocket_command.AppendArg(kPyWebSocketPortNumber); 168 pywebsocket_command.AppendArg("-d"); 169 pywebsocket_command.AppendArgPath(path_to_data_handler); 170 171 VLOG(0) << "Running " << pywebsocket_command.GetCommandLineString(); 172 return base::LaunchProcess(pywebsocket_command, base::LaunchOptions(), 173 &pywebsocket_server_); 174 } 175 176 bool ShutdownPyWebSocketServer() { 177 return base::KillProcess(pywebsocket_server_, 0, false); 178 } 179 180 void EstablishCall(content::WebContents* from_tab, 181 content::WebContents* to_tab) { 182 EXPECT_EQ("ok-peerconnection-created", 183 ExecuteJavascript("preparePeerConnection()", from_tab)); 184 EXPECT_EQ("ok-added", ExecuteJavascript("addLocalStream()", from_tab)); 185 EXPECT_EQ("ok-negotiating", ExecuteJavascript("negotiateCall()", from_tab)); 186 187 // Ensure the call gets up on both sides. 188 EXPECT_TRUE(PollingWaitUntil( 189 "getPeerConnectionReadyState()", "active", from_tab)); 190 EXPECT_TRUE(PollingWaitUntil( 191 "getPeerConnectionReadyState()", "active", to_tab)); 192 } 193 194 void HangUp(content::WebContents* from_tab) { 195 EXPECT_EQ("ok-call-hung-up", ExecuteJavascript("hangUp()", from_tab)); 196 } 197 198 void WaitUntilHangupVerified(content::WebContents* tab_contents) { 199 EXPECT_TRUE(PollingWaitUntil( 200 "getPeerConnectionReadyState()", "no-peer-connection", tab_contents)); 201 } 202 203 // Runs the RGBA to I420 converter on the video in |capture_video_filename|, 204 // which should contain frames of size |width| x |height|. 205 // 206 // The rgba_to_i420_converter is part of the webrtc_test_tools target which 207 // should be build prior to running this test. The resulting binary should 208 // live next to Chrome. 209 bool RunARGBtoI420Converter(int width, 210 int height, 211 const base::FilePath& captured_video_filename) { 212 base::FilePath path_to_converter = base::MakeAbsoluteFilePath( 213 GetBrowserDir().Append(kArgbToI420ConverterExecutable)); 214 215 if (!base::PathExists(path_to_converter)) { 216 LOG(ERROR) << "Missing ARGB->I420 converter: should be in " 217 << path_to_converter.value(); 218 return false; 219 } 220 221 CommandLine converter_command(path_to_converter); 222 converter_command.AppendSwitchPath("--frames_dir", GetWorkingDir()); 223 converter_command.AppendSwitchPath("--output_file", 224 captured_video_filename); 225 converter_command.AppendSwitchASCII("--width", 226 base::StringPrintf("%d", width)); 227 converter_command.AppendSwitchASCII("--height", 228 base::StringPrintf("%d", height)); 229 230 // We produce an output file that will later be used as an input to the 231 // barcode decoder and frame analyzer tools. 232 VLOG(0) << "Running " << converter_command.GetCommandLineString(); 233 std::string result; 234 bool ok = base::GetAppOutput(converter_command, &result); 235 VLOG(0) << "Output was:\n\n" << result; 236 return ok; 237 } 238 239 // Compares the |captured_video_filename| with the |reference_video_filename|. 240 // 241 // The barcode decoder decodes the captured video containing barcodes overlaid 242 // into every frame of the video (produced by rgba_to_i420_converter). It 243 // produces a set of PNG images and a |stats_file| that maps each captured 244 // frame to a frame in the reference video. The frames should be of size 245 // |width| x |height|. 246 // All measurements calculated are printed as perf parsable numbers to stdout. 247 bool CompareVideosAndPrintResult( 248 int width, 249 int height, 250 const base::FilePath& captured_video_filename, 251 const base::FilePath& reference_video_filename, 252 const base::FilePath& stats_file) { 253 254 base::FilePath path_to_analyzer = base::MakeAbsoluteFilePath( 255 GetBrowserDir().Append(kFrameAnalyzerExecutable)); 256 base::FilePath path_to_compare_script = GetSourceDir().Append( 257 FILE_PATH_LITERAL("third_party/webrtc/tools/compare_videos.py")); 258 259 if (!base::PathExists(path_to_analyzer)) { 260 LOG(ERROR) << "Missing frame analyzer: should be in " 261 << path_to_analyzer.value(); 262 return false; 263 } 264 if (!base::PathExists(path_to_compare_script)) { 265 LOG(ERROR) << "Missing video compare script: should be in " 266 << path_to_compare_script.value(); 267 return false; 268 } 269 270 // Note: don't append switches to this command since it will mess up the 271 // -u in the python invocation! 272 CommandLine compare_command(CommandLine::NO_PROGRAM); 273 EXPECT_TRUE(GetPythonCommand(&compare_command)); 274 275 compare_command.AppendArgPath(path_to_compare_script); 276 compare_command.AppendArg("--label=VGA"); 277 compare_command.AppendArg("--ref_video"); 278 compare_command.AppendArgPath(reference_video_filename); 279 compare_command.AppendArg("--test_video"); 280 compare_command.AppendArgPath(captured_video_filename); 281 compare_command.AppendArg("--frame_analyzer"); 282 compare_command.AppendArgPath(path_to_analyzer); 283 compare_command.AppendArg("--yuv_frame_width"); 284 compare_command.AppendArg(base::StringPrintf("%d", width)); 285 compare_command.AppendArg("--yuv_frame_height"); 286 compare_command.AppendArg(base::StringPrintf("%d", height)); 287 compare_command.AppendArg("--stats_file"); 288 compare_command.AppendArgPath(stats_file); 289 290 VLOG(0) << "Running " << compare_command.GetCommandLineString(); 291 std::string output; 292 bool ok = base::GetAppOutput(compare_command, &output); 293 // Print to stdout to ensure the perf numbers are parsed properly by the 294 // buildbot step. 295 printf("Output was:\n\n%s\n", output.c_str()); 296 return ok; 297 } 298 299 base::FilePath GetWorkingDir() { 300 std::string home_dir; 301 environment_->GetVar(kHomeEnvName, &home_dir); 302 base::FilePath::StringType native_home_dir(home_dir.begin(), 303 home_dir.end()); 304 return base::FilePath(native_home_dir).Append(kWorkingDirName); 305 } 306 307 PeerConnectionServerRunner peerconnection_server_; 308 309 private: 310 base::FilePath GetSourceDir() { 311 base::FilePath source_dir; 312 PathService::Get(base::DIR_SOURCE_ROOT, &source_dir); 313 return source_dir; 314 } 315 316 base::FilePath GetBrowserDir() { 317 base::FilePath browser_dir; 318 EXPECT_TRUE(PathService::Get(base::DIR_MODULE, &browser_dir)); 319 return browser_dir; 320 } 321 322 base::ProcessHandle pywebsocket_server_; 323 scoped_ptr<base::Environment> environment_; 324}; 325 326IN_PROC_BROWSER_TEST_F(WebRtcVideoQualityBrowserTest, 327 MANUAL_TestVGAVideoQuality) { 328 ASSERT_GE(TestTimeouts::action_max_timeout().InSeconds(), 150) << 329 "This is a long-running test; you must specify " 330 "--ui-test-action-max-timeout to have a value of at least 150000."; 331 332 ASSERT_TRUE(HasAllRequiredResources()); 333 ASSERT_TRUE(embedded_test_server()->InitializeAndWaitUntilReady()); 334 ASSERT_TRUE(StartPyWebSocketServer()); 335 ASSERT_TRUE(peerconnection_server_.Start()); 336 337 content::WebContents* left_tab = 338 OpenPageAndGetUserMediaInNewTab( 339 embedded_test_server()->GetURL(kMainWebrtcTestHtmlPage)); 340 content::WebContents* right_tab = 341 OpenPageAndGetUserMediaInNewTab( 342 embedded_test_server()->GetURL(kCapturingWebrtcHtmlPage)); 343 344 ConnectToPeerConnectionServer("peer 1", left_tab); 345 ConnectToPeerConnectionServer("peer 2", right_tab); 346 347 EstablishCall(left_tab, right_tab); 348 349 // Poll slower here to avoid flooding the log with messages: capturing and 350 // sending frames take quite a bit of time. 351 int polling_interval_msec = 1000; 352 353 EXPECT_TRUE(PollingWaitUntil( 354 "doneFrameCapturing()", "done-capturing", right_tab, 355 polling_interval_msec)); 356 357 HangUp(left_tab); 358 WaitUntilHangupVerified(left_tab); 359 WaitUntilHangupVerified(right_tab); 360 361 EXPECT_TRUE(PollingWaitUntil( 362 "haveMoreFramesToSend()", "no-more-frames", right_tab, 363 polling_interval_msec)); 364 365 // Shut everything down to avoid having the javascript race with the analysis 366 // tools. For instance, dont have console log printouts interleave with the 367 // RESULT lines from the analysis tools (crbug.com/323200). 368 ASSERT_TRUE(peerconnection_server_.Stop()); 369 ASSERT_TRUE(ShutdownPyWebSocketServer()); 370 371 chrome::CloseWebContents(browser(), left_tab, false); 372 chrome::CloseWebContents(browser(), right_tab, false); 373 374 RunARGBtoI420Converter( 375 kVgaWidth, kVgaHeight, GetWorkingDir().Append(kCapturedYuvFileName)); 376 ASSERT_TRUE( 377 CompareVideosAndPrintResult(kVgaWidth, 378 kVgaHeight, 379 GetWorkingDir().Append(kCapturedYuvFileName), 380 GetWorkingDir().Append(kReferenceYuvFileName), 381 GetWorkingDir().Append(kStatsFileName))); 382} 383