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