1# Copyright (c) 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 5import logging 6import operator 7import os 8import time 9 10from autotest_lib.client.bin import test 11from autotest_lib.client.common_lib import error 12from autotest_lib.client.common_lib.cros import chrome 13from autotest_lib.client.cros.input_playback import keyboard, stylus 14from autotest_lib.client.cros.video import helper_logger 15 16 17class video_YouTubePage(test.test): 18 """The main test class of this test. 19 20 """ 21 22 23 version = 1 24 25 PSEUDO_RANDOM_TIME_1 = 20.25 26 PSEUDO_RANDOM_TIME_2 = 5.47 27 28 # Minimum number of timeupdates required to fire in the last second. 29 MIN_LAST_SECOND_UPDATES = 3 30 31 NO_DELAY = 0 32 MINIMAL_DELAY = 1 33 MAX_REBUFFER_DELAY = 10 34 35 PLAYING_STATE = 'playing' 36 PAUSED_STATE = 'paused' 37 ENDED_STATE = 'ended' 38 39 DISABLE_COOKIES = False 40 41 DROPPED_FRAMES_PERCENT_THRESHOLD = 1 42 DROPPED_FRAMES_DESCRIPTION = 'video_dropped_frames' 43 DROPPED_FRAMES_PERCENT_DESCRIPTION = 'video_dropped_frames_percent' 44 45 tab = None 46 47 48 def initialize_test(self, chrome, player_page): 49 """Initializes the test. 50 51 @param chrome: An Autotest Chrome instance. 52 @param player_page: The URL (string) of the YouTube player page to test. 53 54 """ 55 self.tab = chrome.browser.tabs[0] 56 57 self.tab.Navigate(player_page) 58 self.tab.WaitForDocumentReadyStateToBeComplete() 59 time.sleep(2) 60 61 self.keys = keyboard.Keyboard() 62 63 with open( 64 os.path.join(os.path.dirname(__file__), 65 'files/video_YouTubePageCommon.js')) as f: 66 js = f.read() 67 if not self.tab.EvaluateJavaScript(js): 68 raise error.TestFail('YouTube page failed to load.') 69 logging.info('Loaded accompanying .js script.') 70 71 72 def get_player_state(self): 73 """Simple wrapper to get the JS player state. 74 75 @returns: The state of the player (string). 76 77 """ 78 return self.tab.EvaluateJavaScript('window.__getVideoState();') 79 80 81 def play_video(self): 82 """Simple wrapper to play the video. 83 84 """ 85 self.tab.ExecuteJavaScript('window.__playVideo();') 86 87 88 def pause_video(self): 89 """Simple wrapper to pause the video. 90 91 """ 92 self.tab.ExecuteJavaScript('window.__pauseVideo();') 93 94 95 def seek_video(self, new_time): 96 """Simple wrapper to seek the video to a new time. 97 98 @param new_time: Time to seek to. 99 100 """ 101 self.tab.ExecuteJavaScript('window.__seek(%f);' % new_time) 102 103 104 def seek_to_almost_end(self, seconds_before_end): 105 """Simple wrapper to seek to almost the end of the video. 106 107 @param seconds_before_end: How many seconds (a float, not integer) 108 before end of video. 109 110 """ 111 self.tab.ExecuteJavaScript( 112 'window.__seekToAlmostEnd(%f);' % seconds_before_end) 113 114 115 def get_current_time(self): 116 """Simple wrapper to get the current time in the video. 117 118 @returns: The current time (float). 119 120 """ 121 return self.tab.EvaluateJavaScript('window.__getCurrentTime();') 122 123 124 def is_currently_fullscreen(self): 125 """Simple wrapper to get the current state of fullscreen. 126 127 @returns: True if an element is currently fullscreen. False otherwise. 128 129 """ 130 return self.tab.EvaluateJavaScript('window.__isCurrentlyFullscreen();') 131 132 133 def toggle_fullscreen(self, max_wait_secs=5): 134 """Toggle fullscreen through the YouTube hotkey f. 135 136 @raises: A error.TestError if the fullscreen state does not change. 137 138 """ 139 start_state = self.is_currently_fullscreen() 140 start_time = time.time() 141 142 self.keys.press_key('f') 143 144 while True: 145 current_state = self.is_currently_fullscreen() 146 if current_state != start_state: 147 return 148 elif time.time() < start_time + max_wait_secs: 149 time.sleep(0.5) 150 else: 151 msg = 'Fullscreen did not transition from {} to {}.'.format( 152 start_state, not start_state) 153 raise error.TestError(msg) 154 155 156 def get_frames_statistics(self): 157 """Simple wrapper to get a dictionary of raw video frame states 158 159 @returns: Dict of droppedFrameCount (int), decodedFrameCount (int), and 160 droppedFramesPercentage (float). 161 162 """ 163 return self.tab.EvaluateJavaScript('window.__getFramesStatistics();') 164 165 166 def get_dropped_frame_count(self): 167 """Simple wrapper to get the number of dropped frames. 168 169 @returns: Dropped frame count (int). 170 171 """ 172 return self.get_frames_statistics()['droppedFrameCount'] 173 174 175 def get_dropped_frames_percentage(self): 176 """Simple wrapper to get the percentage of dropped frames. 177 178 @returns: Drop frame percentage (float). 179 180 """ 181 return self.get_frames_statistics()['droppedFramesPercentage'] 182 183 184 def assert_event_state(self, event, op, error_str): 185 """Simple wrapper to get the status of a state in the video. 186 187 @param event: A string denoting the event. Check the accompanying JS 188 file for the possible values. 189 @param op: truth or not_ operator from the standard Python operator 190 module. 191 @param error_str: A string for the error output. 192 193 @returns: Whether or not the input event has fired. 194 195 """ 196 result = self.tab.EvaluateJavaScript( 197 'window.__getEventHappened("%s");' % event) 198 if not op(result): 199 raise error.TestError(error) 200 201 202 def clear_event_state(self, event): 203 """Simple wrapper to clear the status of a state in the video. 204 205 @param event: A string denoting the event. Check the accompanying JS 206 file for the possible vlaues. 207 208 """ 209 self.tab.ExecuteJavaScript('window.__clearEventHappened("%s");' % event) 210 211 212 def verify_last_second_playback(self): 213 """Simple wrapper to check the playback of the last second. 214 215 """ 216 result = self.tab.EvaluateJavaScript( 217 'window.__getLastSecondTimeupdates()') 218 if result < self.MIN_LAST_SECOND_UPDATES: 219 raise error.TestError( 220 'Last second did not play back correctly (%d events).' % 221 result) 222 223 224 def assert_player_state(self, state, max_wait_secs): 225 """Simple wrapper to busy wait and test the current state of the player. 226 227 @param state: A string denoting the expected state of the player. 228 @param max_wait_secs: Maximum amount of time to wait before failing. 229 230 @raises: A error.TestError if the state is not as expected. 231 232 """ 233 start_time = time.time() 234 while True: 235 current_state = self.get_player_state() 236 if current_state == state: 237 return 238 elif time.time() < start_time + max_wait_secs: 239 time.sleep(0.5) 240 else: 241 raise error.TestError( 242 'Current player state "%s" is not the expected state ' 243 '"%s".' % (current_state, state)) 244 245 246 def perform_test(self): 247 """Base method for derived classes to run their test. 248 249 """ 250 raise error.TestFail('Derived class did not specify a perform_test.') 251 252 253 def perform_playing_test(self): 254 """Test to check if the YT page starts off playing. 255 256 """ 257 self.assert_player_state(self.PLAYING_STATE, self.NO_DELAY) 258 if self.get_current_time() <= 0.0: 259 raise error.TestError('perform_playing_test failed.') 260 261 262 def perform_pausing_test(self): 263 """Test to check if the video is in the 'paused' state. 264 265 """ 266 self.assert_player_state(self.PLAYING_STATE, self.NO_DELAY) 267 self.pause_video() 268 self.assert_player_state(self.PAUSED_STATE, self.MINIMAL_DELAY) 269 270 271 def perform_resuming_test(self): 272 """Test to check if the video responds to resumption. 273 274 """ 275 self.assert_player_state(self.PLAYING_STATE, self.NO_DELAY) 276 self.pause_video() 277 self.assert_player_state(self.PAUSED_STATE, self.MINIMAL_DELAY) 278 self.play_video() 279 self.assert_player_state(self.PLAYING_STATE, self.MINIMAL_DELAY) 280 281 282 def perform_seeking_test(self): 283 """Test to check if seeking works. 284 285 """ 286 # Test seeking while playing. 287 self.assert_player_state(self.PLAYING_STATE, self.NO_DELAY) 288 self.seek_video(self.PSEUDO_RANDOM_TIME_1) 289 time.sleep(self.MINIMAL_DELAY) 290 if not self.tab.EvaluateJavaScript( 291 'window.__getCurrentTime() >= %f;' % self.PSEUDO_RANDOM_TIME_1): 292 raise error.TestError( 293 'perform_seeking_test failed because player time is not ' 294 'the expected time during playing seeking.') 295 self.assert_event_state( 296 'seeking', operator.truth, 297 'perform_seeking_test failed: "seeking" state did not fire.') 298 self.assert_event_state( 299 'seeked', operator.truth, 300 'perform_seeking_test failed: "seeked" state did not fire.') 301 302 # Now make sure the video is still playing. 303 304 # Let it buffer/play for at most 10 seconds before continuing. 305 self.assert_player_state(self.PLAYING_STATE, self.MAX_REBUFFER_DELAY) 306 307 self.clear_event_state('seeking'); 308 self.clear_event_state('seeked'); 309 self.assert_event_state( 310 'seeking', operator.not_, 311 'perform_seeking_test failed: ' 312 '"seeking" state did not get cleared.') 313 self.assert_event_state( 314 'seeked', operator.not_, 315 'perform_seeking_test failed: ' 316 '"seeked" state did not get cleared.') 317 318 # Test seeking while paused. 319 self.pause_video() 320 self.assert_player_state(self.PAUSED_STATE, self.MINIMAL_DELAY) 321 322 self.seek_video(self.PSEUDO_RANDOM_TIME_2) 323 time.sleep(self.MINIMAL_DELAY) 324 if not self.tab.EvaluateJavaScript( 325 'window.__getCurrentTime() === %f;' % 326 self.PSEUDO_RANDOM_TIME_2): 327 raise error.TestError( 328 'perform_seeking_test failed because player time is not ' 329 'the expected time.') 330 self.assert_event_state( 331 'seeking', operator.truth, 332 'perform_seeking_test failed: "seeking" state did not fire ' 333 'again.') 334 self.assert_event_state( 335 'seeked', operator.truth, 336 'perform_seeking_test failed: "seeked" state did not fire ' 337 'again.') 338 339 # Make sure the video is paused. 340 self.assert_player_state(self.PAUSED_STATE, self.NO_DELAY) 341 342 343 def perform_frame_drop_test(self): 344 """Test to check if there are too many dropped frames. 345 346 """ 347 self.assert_player_state(self.PLAYING_STATE, self.NO_DELAY) 348 time.sleep(15) 349 dropped_frames_percentage = self.get_dropped_frames_percentage() 350 if dropped_frames_percentage > self.DROPPED_FRAMES_PERCENT_THRESHOLD: 351 raise error.TestError(( 352 'perform_frame_drop_test failed due to too many dropped ' 353 'frames (%f%%)') % (dropped_frames_percentage)) 354 355 356 def perform_fullscreen_test(self): 357 """Test to check if there are too many dropped frames. 358 359 """ 360 # Number of seconds either fullscreened or not 361 state_duration = 2 362 # Number of fullscreen 363 cycles = 5 364 365 # Wait for the player to start 366 self.assert_player_state(self.PLAYING_STATE, self.NO_DELAY) 367 368 # Focus on the browser tab. The second click hides the Stylus help 369 # dialogue popup 370 with stylus.Stylus() as s: 371 s.click_with_percentage(0, 0.5) 372 time.sleep(2) 373 s.click_with_percentage(0, 0.5) 374 375 for _ in range(cycles): 376 # To fullscreen 377 self.toggle_fullscreen() 378 time.sleep(state_duration) 379 380 # Close fullscreen 381 self.toggle_fullscreen() 382 time.sleep(state_duration) 383 384 dropped_frames_percentage = self.get_dropped_frames_percentage() 385 if dropped_frames_percentage > self.DROPPED_FRAMES_PERCENT_THRESHOLD: 386 raise error.TestError(( 387 'perform_frame_drop_test failed due to too many dropped ' 388 'frames (%f%%)') % (dropped_frames_percentage)) 389 390 391 def perform_ending_test(self): 392 """Test to check if the state is 'ended' at the end of a video. 393 394 """ 395 ALMOST_END = 0.5 396 self.assert_player_state(self.PLAYING_STATE, self.NO_DELAY) 397 self.seek_to_almost_end(ALMOST_END) 398 self.assert_player_state(self.ENDED_STATE, self.MAX_REBUFFER_DELAY) 399 400 401 def perform_last_second_test(self): 402 """Test to check if the last second is played. 403 404 """ 405 NEAR_END = 2.0 406 self.assert_player_state(self.PLAYING_STATE, self.NO_DELAY) 407 self.seek_to_almost_end(NEAR_END) 408 self.assert_player_state( 409 self.ENDED_STATE, self.MAX_REBUFFER_DELAY + NEAR_END) 410 self.verify_last_second_playback() 411 412 413 def perform_360_test(self): 414 """Test to measure the number of dropped frames while playing a YouTube 415 3D 360 video. 416 417 """ 418 # Operations, repetitions, and their post-operation delay. 3D 360 419 # degree video isn't any fun if you don't try moving around :). 420 operations = [('d', 3, 2), ('d', 3, 2), ('d', 5, 5), 421 ('w', 3, 2), ('w', 3, 2), ('s', 3, 5), 422 ('a', 4, 10), ('a', 3, 10), ('a', 3, 10)] 423 424 # Wait for the player to start 425 self.assert_player_state(self.PLAYING_STATE, self.NO_DELAY) 426 427 with stylus.Stylus() as s: 428 # Focus on the browser tab. 429 s.click_with_percentage(0, 0.5) 430 time.sleep(2) 431 432 self.toggle_fullscreen() 433 time.sleep(2) 434 435 # Focus on the fullscreen content. Second click unpauses. 436 s.click_with_percentage(0, 0.5) 437 s.click_with_percentage(0, 0.5) 438 439 # Navigating a YouTube 360 degree video is done with GUI interactions 440 # or a combination of WASD and mouse scrolling. W and S pitch the 441 # field of view up and down respectively. A and D then rotate left 442 # and right. Zooming is handled by scrolling up and down. For example, 443 # we might tap D three times to rotate a bit and then pause to watch 444 # the new field of view. 445 for operation, repititions, delay in operations: 446 for _ in range(repititions): 447 self.keys.press_key(operation) 448 time.sleep(delay) 449 450 dropped_frame_count = self.get_dropped_frame_count() 451 dropped_frames_percentage = self.get_dropped_frames_percentage() 452 453 # Record frame stats 454 self.output_perf_value( 455 description=self.DROPPED_FRAMES_DESCRIPTION, 456 value=dropped_frame_count, units='frames', higher_is_better=False, 457 graph=None) 458 self.output_perf_value( 459 description=self.DROPPED_FRAMES_PERCENT_DESCRIPTION, 460 value=dropped_frames_percentage, units='percent', 461 higher_is_better=False, graph=None) 462 463 464 @helper_logger.video_log_wrapper 465 def run_once(self, subtest_name, test_page): 466 """Main runner for the test. 467 468 @param subtest_name: The name of the test to run, given below. 469 470 """ 471 extension_paths = [] 472 if self.DISABLE_COOKIES: 473 # To stop the system from erasing the previous profile, enable: 474 # options.dont_override_profile = True 475 extension_path = os.path.join( 476 os.path.dirname(__file__), 477 'files/cookie-disabler') 478 extension_paths.append(extension_path) 479 480 with chrome.Chrome( 481 extra_browser_args=helper_logger.chrome_vmodule_flag(), 482 extension_paths=extension_paths) as cr: 483 self.initialize_test(cr, test_page) 484 485 if subtest_name is 'playing': 486 self.perform_playing_test() 487 elif subtest_name is 'pausing': 488 self.perform_pausing_test() 489 elif subtest_name is 'resuming': 490 self.perform_resuming_test() 491 elif subtest_name is 'seeking': 492 self.perform_seeking_test() 493 elif subtest_name is 'frame_drop': 494 self.perform_frame_drop_test() 495 elif subtest_name is 'fullscreen': 496 self.perform_fullscreen_test() 497 elif subtest_name is 'ending': 498 self.perform_ending_test() 499 elif subtest_name is 'last_second': 500 self.perform_last_second_test() 501 elif subtest_name is '360': 502 self.perform_360_test() 503