video.py 30.2 KB
Newer Older
1 2 3
"""
Video player in the courseware.
"""
4

5
import time
Alexander Kryklia committed
6
import json
7 8
import requests
from selenium.webdriver.common.action_chains import ActionChains
9
from bok_choy.page_object import PageObject
10
from bok_choy.promise import EmptyPromise, Promise
11
from bok_choy.javascript import wait_for_js, js_defined
12

13 14
import logging
log = logging.getLogger('VideoPage')
15

16
VIDEO_BUTTONS = {
17
    'transcript': '.language-menu',
18
    'transcript_button': '.toggle-transcript',
19
    'cc_button': '.toggle-captions',
20 21 22 23 24
    'volume': '.volume',
    'play': '.video_control.play',
    'pause': '.video_control.pause',
    'fullscreen': '.add-fullscreen',
    'download_transcript': '.video-tracks > a',
25 26
    'speed': '.speeds',
    'quality': '.quality-control',
Alexander Kryklia committed
27 28
    'do_not_show_again': '.skip-control',
    'skip_bumper': '.play-skip-control',
29 30 31
}

CSS_CLASS_NAMES = {
32
    'captions_closed': '.video.closed',
33
    'captions_rendered': '.video.is-captions-rendered',
34
    'captions': '.subtitles',
35 36 37
    'captions_text': '.subtitles li',
    'captions_text_getter': '.subtitles li[role="link"][data-index="1"]',
    'closed_captions': '.closed-captions',
38
    'error_message': '.video .video-player .video-error',
39
    'video_container': '.video',
40 41
    'video_sources': '.video-player video source',
    'video_spinner': '.video-wrapper .spinner',
42 43
    'video_xmodule': '.xmodule_VideoModule',
    'video_init': '.is-initialized',
44
    'video_time': '.vidtime',
45
    'video_display_name': '.vert h3',
46
    'captions_lang_list': '.langs-list li',
Alexander Kryklia committed
47 48
    'video_speed': '.speeds .value',
    'poster': '.poster',
49 50 51
}

VIDEO_MODES = {
52 53
    'html5': '.video video',
    'youtube': '.video iframe'
54 55
}

56 57 58 59
VIDEO_MENUS = {
    'language': '.lang .menu',
    'speed': '.speed .menu',
    'download_transcript': '.video-tracks .a11y-menu-list',
60 61
    'transcript-format': '.video-tracks .a11y-menu-button',
    'transcript-skip': '.sr-is-focusable.transcript-start',
62 63 64
}


65 66
@js_defined('window.Video', 'window.RequireJS.require', 'window.jQuery',
            'window.MathJax', 'window.MathJax.isReady')
67 68 69 70 71
class VideoPage(PageObject):
    """
    Video player in the courseware.
    """

72
    url = None
73
    current_video_display_name = None
74

75
    @wait_for_js
76
    def is_browser_on_page(self):
77
        return self.q(css='div{0}'.format(CSS_CLASS_NAMES['video_xmodule'])).present
78

79 80
    @wait_for_js
    def wait_for_video_class(self):
81
        """
82
        Wait until element with class name `video` appeared in DOM.
83

84
        """
85
        self.wait_for_ajax()
86

87
        video_selector = '{0}'.format(CSS_CLASS_NAMES['video_container'])
88
        self.wait_for_element_presence(video_selector, 'Video is initialized')
Muhammad Ammar committed
89

90
    @wait_for_js
Alexander Kryklia committed
91
    def wait_for_video_player_render(self, autoplay=False):
92 93
        """
        Wait until Video Player Rendered Completely.
94

95
        """
96
        self.wait_for_video_class()
97 98
        self.wait_for_element_presence(CSS_CLASS_NAMES['video_init'], 'Video Player Initialized')
        self.wait_for_element_presence(CSS_CLASS_NAMES['video_time'], 'Video Player Initialized')
99

Alexander Kryklia committed
100 101 102 103 104 105
        video_player_buttons = ['volume', 'fullscreen', 'speed']
        if autoplay:
            video_player_buttons.append('pause')
        else:
            video_player_buttons.append('play')

Muhammad Ammar committed
106
        for button in video_player_buttons:
107
            self.wait_for_element_visibility(VIDEO_BUTTONS[button], '{} button is visible'.format(button))
Muhammad Ammar committed
108

109 110
        def _is_finished_loading():
            """
111 112 113 114 115
            Check if video loading completed.

            Returns:
                bool: Tells Video Finished Loading.

116 117 118
            """
            return not self.q(css=CSS_CLASS_NAMES['video_spinner']).visible

119 120
        EmptyPromise(_is_finished_loading, 'Finished loading the video', timeout=200).fulfill()

121 122
        self.wait_for_ajax()

Alexander Kryklia committed
123 124 125 126 127 128 129 130 131 132 133
    @wait_for_js
    def wait_for_video_bumper_render(self):
        """
        Wait until Poster, Video Pre-Roll and main Video Player are Rendered Completely.
        """
        self.wait_for_video_class()
        self.wait_for_element_presence(CSS_CLASS_NAMES['video_init'], 'Video Player Initialized')
        self.wait_for_element_presence(CSS_CLASS_NAMES['video_time'], 'Video Player Initialized')

        video_player_buttons = ['do_not_show_again', 'skip_bumper', 'volume']
        for button in video_player_buttons:
134
            self.wait_for_element_visibility(VIDEO_BUTTONS[button], '{} button is visible'.format(button))
Alexander Kryklia committed
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150

    @property
    def is_poster_shown(self):
        """
        Check whether a poster is show.
        """
        selector = self.get_element_selector(CSS_CLASS_NAMES['poster'])
        return self.q(css=selector).visible

    def click_on_poster(self):
        """
        Click on the video poster.
        """
        selector = self.get_element_selector(CSS_CLASS_NAMES['poster'])
        self.q(css=selector).click()

151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169
    def get_video_vertical_selector(self, video_display_name=None):
        """
        Get selector for a video vertical with display name specified by `video_display_name`.

        Arguments:
            video_display_name (str or None): Display name of a Video. Default vertical selector if None.

        Returns:
            str: Vertical Selector for video.

        """
        if video_display_name:
            video_display_names = self.q(css=CSS_CLASS_NAMES['video_display_name']).text
            if video_display_name not in video_display_names:
                raise ValueError("Incorrect Video Display Name: '{0}'".format(video_display_name))
            return '.vert.vert-{}'.format(video_display_names.index(video_display_name))
        else:
            return '.vert.vert-0'

170
    def get_element_selector(self, class_name, vertical=True):
171 172 173 174 175
        """
        Construct unique element selector.

        Arguments:
            class_name (str): css class name for an element.
176
            vertical (bool): do we need vertical css selector or not. vertical css selector is not present in Studio
177 178 179

        Returns:
            str: Element Selector.
180

181
        """
182 183 184 185 186 187
        if vertical:
            return '{vertical} {video_element}'.format(
                vertical=self.get_video_vertical_selector(self.current_video_display_name),
                video_element=class_name)
        else:
            return class_name
188

189 190 191 192 193 194 195 196 197 198 199
    def use_video(self, video_display_name):
        """
        Set current video display name.

        Arguments:
            video_display_name (str): Display name of a Video.

        """
        self.current_video_display_name = video_display_name

    def is_video_rendered(self, mode):
200
        """
201
        Check that if video is rendered in `mode`.
202 203 204 205 206 207 208

        Arguments:
            mode (str): Video mode, `html5` or `youtube`.

        Returns:
            bool: Tells if video is rendered in `mode`.

209
        """
210
        selector = self.get_element_selector(VIDEO_MODES[mode])
211 212 213

        def _is_element_present():
            """
214 215 216 217 218 219
            Check if a web element is present in DOM.

            Returns:
                tuple: (is_satisfied, result)`, where `is_satisfied` is a boolean indicating whether the promise was
                satisfied, and `result` is a value to return from the fulfilled `Promise`.

220
            """
221
            is_present = self.q(css=selector).present
222 223 224
            return is_present, is_present

        return Promise(_is_element_present, 'Video Rendering Failed in {0} mode.'.format(mode)).fulfill()
225

226 227
    @property
    def is_autoplay_enabled(self):
228
        """
Alexander Kryklia committed
229
        Extract autoplay value of `data-metadata` attribute to check video autoplay is enabled or disabled.
230 231 232

        Returns:
            bool: Tells if autoplay enabled/disabled.
233
        """
234
        selector = self.get_element_selector(CSS_CLASS_NAMES['video_container'])
Alexander Kryklia committed
235 236
        auto_play = json.loads(self.q(css=selector).attrs('data-metadata')[0])['autoplay']
        return auto_play
237

238 239
    @property
    def is_error_message_shown(self):
240
        """
241
        Checks if video player error message shown.
242 243 244 245

        Returns:
            bool: Tells about error message visibility.

246
        """
247
        selector = self.get_element_selector(CSS_CLASS_NAMES['error_message'])
248
        return self.q(css=selector).visible
249

250 251
    @property
    def is_spinner_shown(self):
252 253 254 255 256 257 258
        """
        Checks if video spinner shown.

        Returns:
            bool: Tells about spinner visibility.

        """
259
        selector = self.get_element_selector(CSS_CLASS_NAMES['video_spinner'])
260 261
        return self.q(css=selector).visible

262 263
    @property
    def error_message_text(self):
264
        """
265
        Extract video player error message text.
266 267 268 269

        Returns:
            str: Error message text.

270
        """
271
        selector = self.get_element_selector(CSS_CLASS_NAMES['error_message'])
272
        return self.q(css=selector).text[0]
273

274
    def is_button_shown(self, button_id):
275
        """
276 277 278 279 280 281 282 283
        Check if a video button specified by `button_id` is visible.

        Arguments:
            button_id (str): key in VIDEO_BUTTONS dictionary, its value will give us the css selector for button.

        Returns:
            bool: Tells about a buttons visibility.

284
        """
285
        selector = self.get_element_selector(VIDEO_BUTTONS[button_id])
286
        return self.q(css=selector).visible
287

288
    def show_captions(self):
289
        """
290 291
        Make Captions Visible.
        """
292
        self._captions_visibility(True)
293

294
    def hide_captions(self):
295 296 297
        """
        Make Captions Invisible.
        """
298
        self._captions_visibility(False)
299

300 301 302 303 304 305 306 307 308 309 310 311
    def show_closed_captions(self):
        """
        Make closed captions visible.
        """
        self._closed_captions_visibility(True)

    def hide_closed_captions(self):
        """
        Make closed captions invisible.
        """
        self._closed_captions_visibility(False)

312 313 314 315 316 317 318 319
    def is_captions_visible(self):
        """
        Get current visibility sate of captions.

        Returns:
            bool: True means captions are visible, False means captions are not visible

        """
Alexander Kryklia committed
320
        self.wait_for_ajax()
321 322 323 324 325 326 327 328 329 330 331 332 333 334
        caption_state_selector = self.get_element_selector(CSS_CLASS_NAMES['captions'])
        return self.q(css=caption_state_selector).visible

    def is_closed_captions_visible(self):
        """
        Get current visibility sate of closed captions.

        Returns:
            bool: True means captions are visible, False means captions are not visible

        """
        self.wait_for_ajax()
        closed_caption_state_selector = self.get_element_selector(CSS_CLASS_NAMES['closed_captions'])
        return self.q(css=closed_caption_state_selector).visible
335

336
    @wait_for_js
337
    def _captions_visibility(self, captions_new_state):
338 339
        """
        Set the video captions visibility state.
340 341

        Arguments:
342
            captions_new_state (bool): True means show captions, False means hide captions
343

344
        """
345 346
        states = {True: 'Shown', False: 'Hidden'}
        state = states[captions_new_state]
347

348 349 350
        # Make sure that the transcript button is there
        EmptyPromise(lambda: self.is_button_shown('transcript_button'),
                     "transcript button is shown").fulfill()
351

352
        # toggle captions visibility state if needed
353
        if self.is_captions_visible() != captions_new_state:
354
            self.click_player_button('transcript_button')
355

356
            # Verify that captions state is toggled/changed
357
            EmptyPromise(lambda: self.is_captions_visible() == captions_new_state,
358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375
                         "Transcripts are {state}".format(state=state)).fulfill()

    @wait_for_js
    def _closed_captions_visibility(self, closed_captions_new_state):
        """
        Set the video closed captioning visibility state.

        Arguments:
            closed_captions_new_state (bool): True means show closed captioning
        """
        states = {True: 'shown', False: 'hidden'}
        state = states[closed_captions_new_state]

        self.click_player_button('cc_button')

        # Make sure that the captions are visible
        EmptyPromise(lambda: self.is_closed_captions_visible() == closed_captions_new_state,
                     "Closed captions are {state}".format(state=state)).fulfill()
376

377 378
    @property
    def captions_text(self):
379
        """
380
        Extract captions text.
381 382 383 384

        Returns:
            str: Captions Text.

385
        """
386
        self.wait_for_captions()
387

388
        captions_selector = self.get_element_selector(CSS_CLASS_NAMES['captions_text'])
389
        subs = self.q(css=captions_selector).html
390 391 392

        return ' '.join(subs)

393
    @property
394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418
    def closed_captions_text(self):
        """
        Extract closed captioning text.

        Returns:
            str: closed captions Text.

        """
        self.wait_for_closed_captions()

        closed_captions_selector = self.get_element_selector(CSS_CLASS_NAMES['closed_captions'])
        subs = self.q(css=closed_captions_selector).html

        return ' '.join(subs)

    def click_first_line_in_transcript(self):
        """
        Clicks a line in the transcript updating the current caption.
        """

        self.wait_for_captions()
        captions_selector = self.q(css=CSS_CLASS_NAMES['captions_text_getter'])
        captions_selector.click()

    @property
419 420 421 422 423 424 425 426 427 428 429 430 431
    def speed(self):
        """
        Get current video speed value.

        Return:
            str: speed value

        """
        speed_selector = self.get_element_selector(CSS_CLASS_NAMES['video_speed'])
        return self.q(css=speed_selector).text[0]

    @speed.setter
    def speed(self, speed):
432 433 434
        """
        Change the video play speed.

435 436 437 438 439
        Arguments:
            speed (str): Video speed value

        """
        # mouse over to video speed button
440
        speed_menu_selector = self.get_element_selector(VIDEO_BUTTONS['speed'])
441 442 443
        element_to_hover_over = self.q(css=speed_menu_selector).results[0]
        hover = ActionChains(self.browser).move_to_element(element_to_hover_over)
        hover.perform()
444

445
        speed_selector = self.get_element_selector('li[data-speed="{speed}"] .control'.format(speed=speed))
446
        self.q(css=speed_selector).first.click()
447 448
        # Click triggers an ajax event
        self.wait_for_ajax()
449

450 451 452 453 454 455 456
    def verify_speed_changed(self, expected_speed):
        """
        Wait for the video to change its speed to the expected value. If it does not change,
        the wait call will fail the test.
        """
        self.wait_for(lambda: self.speed == expected_speed, "Video speed changed")

457
    def click_player_button(self, button):
458
        """
459 460 461 462 463
        Click on `button`.

        Arguments:
            button (str): key in VIDEO_BUTTONS dictionary, its value will give us the css selector for `button`

464
        """
465
        button_selector = self.get_element_selector(VIDEO_BUTTONS[button])
466 467 468 469 470

        # If we are going to click pause button, Ensure that player is not in buffering state
        if button == 'pause':
            self.wait_for(lambda: self.state != 'buffering', 'Player is Ready for Pause')

471 472 473
        self.q(css=button_selector).first.click()

        self.wait_for_ajax()
474 475 476 477

    def _get_element_dimensions(self, selector):
        """
        Gets the width and height of element specified by `selector`
478 479 480 481 482 483 484

        Arguments:
            selector (str): css selector of a web element

        Returns:
            dict: Dimensions of a web element.

485 486 487 488
        """
        element = self.q(css=selector).results[0]
        return element.size

489 490
    @property
    def _dimensions(self):
491
        """
492 493 494 495 496
        Gets the video player dimensions.

        Returns:
            tuple: Dimensions

497
        """
498 499
        iframe_selector = self.get_element_selector('.video-player iframe,')
        video_selector = self.get_element_selector(' .video-player video')
500
        video = self._get_element_dimensions(iframe_selector + video_selector)
501 502
        wrapper = self._get_element_dimensions(self.get_element_selector('.tc-wrapper'))
        controls = self._get_element_dimensions(self.get_element_selector('.video-controls'))
503
        progress_slider = self._get_element_dimensions(
504
            self.get_element_selector('.video-controls > .slider'))
505 506 507 508 509 510

        expected = dict(wrapper)
        expected['height'] -= controls['height'] + 0.5 * progress_slider['height']

        return video, expected

511
    def is_aligned(self, is_transcript_visible):
512 513
        """
        Check if video is aligned properly.
514 515 516 517 518 519 520

        Arguments:
            is_transcript_visible (bool): Transcript is visible or not.

        Returns:
            bool: Alignment result.

521 522 523 524 525 526 527 528 529 530 531
        """
        # Width of the video container in css equal 75% of window if transcript enabled
        wrapper_width = 75 if is_transcript_visible else 100
        initial = self.browser.get_window_size()

        self.browser.set_window_size(300, 600)

        # Wait for browser to resize completely
        # Currently there is no other way to wait instead of explicit wait
        time.sleep(0.2)

532
        real, expected = self._dimensions
533 534 535 536 537 538 539 540 541

        width = round(100 * real['width'] / expected['width']) == wrapper_width

        self.browser.set_window_size(600, 300)

        # Wait for browser to resize completely
        # Currently there is no other way to wait instead of explicit wait
        time.sleep(0.2)

542
        real, expected = self._dimensions
543 544 545 546 547 548 549 550 551 552 553 554

        height = abs(expected['height'] - real['height']) <= 5

        # Restore initial window size
        self.browser.set_window_size(
            initial['width'], initial['height']
        )

        return all([width, height])

    def _get_transcript(self, url):
        """
555 556
        Download Transcript from `url`

557 558 559 560 561 562 563 564 565 566 567 568
        """
        kwargs = dict()

        session_id = [{i['name']: i['value']} for i in self.browser.get_cookies() if i['name'] == u'sessionid']
        if session_id:
            kwargs.update({
                'cookies': session_id[0]
            })

        response = requests.get(url, **kwargs)
        return response.status_code < 400, response.headers, response.content

569 570 571 572 573 574
    def get_cookie(self, cookie_name):
        """
        Searches for and returns `cookie_name`
        """
        return self.browser.get_cookie(cookie_name)

575
    def downloaded_transcript_contains_text(self, transcript_format, text_to_search):
576 577
        """
        Download the transcript in format `transcript_format` and check that it contains the text `text_to_search`
578 579 580 581 582 583 584 585

        Arguments:
            transcript_format (str): Transcript file format `srt` or `txt`
            text_to_search (str): Text to search in Transcript.

        Returns:
            bool: Transcript download result.

586
        """
587
        transcript_selector = self.get_element_selector(VIDEO_MENUS['transcript-format'])
588

589
        # check if we have a transcript with correct format
590 591
        if '.' + transcript_format not in self.q(css=transcript_selector).text[0]:
            return False
592 593 594 595 596 597

        formats = {
            'srt': 'application/x-subrip',
            'txt': 'text/plain',
        }

598
        transcript_url_selector = self.get_element_selector(VIDEO_BUTTONS['download_transcript'])
599
        url = self.q(css=transcript_url_selector).attrs('href')[0]
600 601
        result, headers, content = self._get_transcript(url)

602 603
        if result is False:
            return False
604

605 606 607 608 609 610 611 612
        if formats[transcript_format] not in headers.get('content-type', ''):
            return False

        if text_to_search not in content.decode('utf-8'):
            return False

        return True

613 614 615 616 617
    def current_language(self):
        """
        Get current selected video transcript language.
        """
        selector = self.get_element_selector(VIDEO_MENUS["language"] + ' li.is-active')
polesye committed
618 619
        return self.q(css=selector).first.attrs('data-lang-code')[0]

620
    def select_language(self, code):
621
        """
622
        Select captions for language `code`.
623

624 625 626 627 628
        Arguments:
            code (str): two character language code like `en`, `zh`.

        """
        self.wait_for_ajax()
629

630 631
        # mouse over to transcript button
        cc_button_selector = self.get_element_selector(VIDEO_BUTTONS["transcript"])
632
        element_to_hover_over = self.q(css=cc_button_selector).results[0]
polesye committed
633
        ActionChains(self.browser).move_to_element(element_to_hover_over).perform()
634

635
        language_selector = VIDEO_MENUS["language"] + ' li[data-lang-code="{code}"]'.format(code=code)
636 637
        language_selector = self.get_element_selector(language_selector)
        self.wait_for_element_visibility(language_selector, 'language menu is visible')
638 639
        self.q(css=language_selector).first.click()

polesye committed
640 641
        # Sometimes language is not clicked correctly. So, if the current language code
        # differs form the expected, we try to change it again.
642 643
        if self.current_language() != code:
            self.select_language(code)
polesye committed
644

645 646
        if 'is-active' != self.q(css=language_selector).attrs('class')[0]:
            return False
647

648
        active_lang_selector = self.get_element_selector(VIDEO_MENUS["language"] + ' li.is-active')
649 650
        if len(self.q(css=active_lang_selector).results) != 1:
            return False
651 652 653

        # Make sure that all ajax requests that affects the display of captions are finished.
        # For example, request to get new translation etc.
654
        self.wait_for_ajax()
655

656
        captions_selector = self.get_element_selector(CSS_CLASS_NAMES['captions'])
657
        EmptyPromise(lambda: self.q(css=captions_selector).visible, 'Subtitles Visible').fulfill()
658

659
        self.wait_for_captions()
660 661 662

        return True

663
    def is_menu_present(self, menu_name):
664 665 666 667 668 669 670 671 672 673
        """
        Check if menu `menu_name` exists.

        Arguments:
            menu_name (str): Menu key from VIDEO_MENUS.

        Returns:
            bool: Menu existence result

        """
674
        selector = self.get_element_selector(VIDEO_MENUS[menu_name])
675 676
        return self.q(css=selector).present

677
    def select_transcript_format(self, transcript_format):
678 679 680 681 682 683 684 685 686 687
        """
        Select transcript with format `transcript_format`.

        Arguments:
            transcript_format (st): Transcript file format `srt` or `txt`.

        Returns:
            bool: Selection Result.

        """
688
        button_selector = self.get_element_selector(VIDEO_MENUS['transcript-format'])
689 690 691 692 693 694 695 696 697

        button = self.q(css=button_selector).results[0]

        hover = ActionChains(self.browser).move_to_element(button)
        hover.perform()

        if '...' not in self.q(css=button_selector).text[0]:
            return False

698
        menu_selector = self.get_element_selector(VIDEO_MENUS['download_transcript'])
699 700 701
        menu_items = self.q(css=menu_selector + ' a').results
        for item in menu_items:
            if item.get_attribute('data-value') == transcript_format:
702
                ActionChains(self.browser).move_to_element(item).click().perform()
703 704 705 706 707 708 709 710 711 712 713 714
                self.wait_for_ajax()
                break

        self.browser.execute_script("window.scrollTo(0, 0);")

        if self.q(css=menu_selector + ' .active a').attrs('data-value')[0] != transcript_format:
            return False

        if '.' + transcript_format not in self.q(css=button_selector).text[0]:
            return False

        return True
715

716 717
    @property
    def sources(self):
718 719 720 721 722 723 724
        """
        Extract all video source urls on current page.

        Returns:
            list: Video Source URLs.

        """
725
        sources_selector = self.get_element_selector(CSS_CLASS_NAMES['video_sources'])
726 727
        return self.q(css=sources_selector).map(lambda el: el.get_attribute('src').split('?')[0]).results

728 729
    @property
    def caption_languages(self):
730 731 732 733 734 735 736
        """
        Get caption languages available for a video.

        Returns:
            dict: Language Codes('en', 'zh' etc) as keys and Language Names as Values('English', 'Chinese' etc)

        """
737
        languages_selector = self.get_element_selector(CSS_CLASS_NAMES['captions_lang_list'])
738 739 740 741
        language_codes = self.q(css=languages_selector).attrs('data-lang-code')
        language_names = self.q(css=languages_selector).attrs('textContent')

        return dict(zip(language_codes, language_names))
742

743 744
    @property
    def position(self):
745 746 747 748 749 750 751
        """
        Gets current video slider position.

        Returns:
            str: current seek position in format min:sec.

        """
752
        selector = self.get_element_selector(CSS_CLASS_NAMES['video_time'])
753 754 755
        current_seek_position = self.q(css=selector).text[0]
        return current_seek_position.split('/')[0].strip()

756 757 758 759 760 761 762
    @property
    def seconds(self):
        """
        Extract seconds part from current video slider position.

        Returns:
            str
Muhammad Ammar committed
763

764
        """
765
        return int(self.position.split(':')[1])
766

767 768 769 770
    @property
    def state(self):
        """
        Extract the current state (play, pause etc) of video.
771 772 773 774 775

        Returns:
            str: current video state

        """
776
        state_selector = self.get_element_selector(CSS_CLASS_NAMES['video_container'])
777 778
        current_state = self.q(css=state_selector).attrs('class')[0]

779 780 781 782 783
        # For troubleshooting purposes show what the current state is.
        # The debug statements will only be displayed in the event of a failure.
        logging.debug("Current state of '{}' element is '{}'".format(state_selector, current_state))

        # See the JS video player's onStateChange function
784 785 786 787 788 789 790 791 792
        if 'is-playing' in current_state:
            return 'playing'
        elif 'is-paused' in current_state:
            return 'pause'
        elif 'is-buffered' in current_state:
            return 'buffering'
        elif 'is-ended' in current_state:
            return 'finished'

793
    def _wait_for(self, check_func, desc, result=False, timeout=200, try_interval=0.2):
794
        """
795
        Calls the method provided as an argument until the Promise satisfied or BrokenPromise
796 797 798 799

        Arguments:
            check_func (callable): Function that accepts no arguments and returns a boolean indicating whether the promise is fulfilled.
            desc (str): Description of the Promise, used in log messages.
800
            result (bool): Indicates whether we need a results from Promise or not
801 802 803 804
            timeout (float): Maximum number of seconds to wait for the Promise to be satisfied before timing out.

        """
        if result:
805
            return Promise(check_func, desc, timeout=timeout, try_interval=try_interval).fulfill()
806
        else:
807
            return EmptyPromise(check_func, desc, timeout=timeout, try_interval=try_interval).fulfill()
808

809
    def wait_for_state(self, state):
810 811 812 813 814 815 816 817
        """
        Wait until `state` occurs.

        Arguments:
            state (str): State we wait for.

        """
        self._wait_for(
818
            lambda: self.state == state,
819 820 821
            'State is {state}'.format(state=state)
        )

822
    def seek(self, seek_value):
823 824 825 826 827 828 829
        """
        Seek the video to position specified by `seek_value`.

        Arguments:
            seek_value (str): seek value

        """
830
        seek_time = _parse_time_str(seek_value)
831
        seek_selector = self.get_element_selector(' .video')
832 833 834
        js_code = "$('{seek_selector}').data('video-player-state').videoPlayer.onSlideSeek({{time: {seek_time}}})".format(
            seek_selector=seek_selector, seek_time=seek_time)
        self.browser.execute_script(js_code)
835

Muhammad Ammar committed
836 837 838 839 840 841
        # after seek, player goes into `is-buffered` state. we need to get
        # out of this state before doing any further operation/action.
        def _is_buffering_completed():
            """
            Check if buffering completed
            """
842
            return self.state != 'buffering'
Muhammad Ammar committed
843 844

        self._wait_for(_is_buffering_completed, 'Buffering completed after Seek.')
845
        self.wait_for_position(seek_value)
Muhammad Ammar committed
846

847 848 849 850 851 852 853
    def reload_page(self):
        """
        Reload/Refresh the current video page.
        """
        self.browser.refresh()
        self.wait_for_video_player_render()

854 855
    @property
    def duration(self):
856 857 858 859 860 861 862
        """
        Extract video duration.

        Returns:
            str: duration in format min:sec

        """
863
        selector = self.get_element_selector(CSS_CLASS_NAMES['video_time'])
864 865 866 867 868 869 870 871

        # The full time has the form "0:32 / 3:14" elapsed/duration
        all_times = self.q(css=selector).text[0]

        duration_str = all_times.split('/')[1]

        return duration_str.strip()

872
    def wait_for_position(self, position):
873 874 875 876 877 878 879 880
        """
        Wait until current will be equal to `position`.

        Arguments:
            position (str): position we wait for.

        """
        self._wait_for(
881
            lambda: self.position == position,
882 883
            'Position is {position}'.format(position=position)
        )
884

885 886
    @property
    def is_quality_button_visible(self):
887 888 889 890 891 892 893
        """
        Get the visibility state of quality button

        Returns:
            bool: visibility status

        """
894
        selector = self.get_element_selector(VIDEO_BUTTONS['quality'])
895 896
        return self.q(css=selector).visible

897 898
    @property
    def is_quality_button_active(self):
899 900 901 902 903 904 905
        """
        Check if quality button is active or not.

        Returns:
            bool: active status

        """
906
        selector = self.get_element_selector(VIDEO_BUTTONS['quality'])
907 908 909

        classes = self.q(css=selector).attrs('class')[0].split()
        return 'active' in classes
910

911 912 913 914 915 916 917 918 919 920 921
    @property
    def is_transcript_skip_visible(self):
        """
        Checks if the skip-to containers in transcripts are present and visible.

        Returns:
            bool
        """
        selector = self.get_element_selector(VIDEO_MENUS['transcript-skip'])
        return self.q(css=selector).visible

922 923 924 925 926 927
    def wait_for_captions(self):
        """
        Wait until captions rendered completely.
        """
        captions_rendered_selector = self.get_element_selector(CSS_CLASS_NAMES['captions_rendered'])
        self.wait_for_element_presence(captions_rendered_selector, 'Captions Rendered')
928

929 930 931 932 933 934 935 936 937 938 939 940 941 942
    def wait_for_closed_captions(self):
        """
        Wait until closed captions are rendered completely.
        """
        cc_rendered_selector = self.get_element_selector(CSS_CLASS_NAMES['closed_captions'])
        self.wait_for_element_visibility(cc_rendered_selector, 'Closed captions rendered')

    def wait_for_closed_captions_to_be_hidden(self):
        """
        Waits for the closed captions to be turned off completely.
        """
        cc_rendered_selector = self.get_element_selector(CSS_CLASS_NAMES['closed_captions'])
        self.wait_for_element_invisibility(cc_rendered_selector, 'Closed captions hidden')

943 944 945 946 947 948 949 950 951 952 953 954 955 956

def _parse_time_str(time_str):
    """
    Parse a string of the form 1:23 into seconds (int).

    Arguments:
        time_str (str): seek value

    Returns:
        int: seek value in seconds

    """
    time_obj = time.strptime(time_str, '%M:%S')
    return time_obj.tm_min * 60 + time_obj.tm_sec