mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-28 03:17:31 +09:00
import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo
This commit is contained in:
commit
dcd9973243
150858 changed files with 23884658 additions and 0 deletions
0
dom/media/test/external/external_media_tests/media_utils/__init__.py
vendored
Normal file
0
dom/media/test/external/external_media_tests/media_utils/__init__.py
vendored
Normal file
448
dom/media/test/external/external_media_tests/media_utils/video_puppeteer.py
vendored
Normal file
448
dom/media/test/external/external_media_tests/media_utils/video_puppeteer.py
vendored
Normal file
|
|
@ -0,0 +1,448 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
from collections import namedtuple
|
||||
from time import clock, sleep
|
||||
|
||||
from marionette_driver import By, expected, Wait
|
||||
from marionette_harness import Marionette
|
||||
|
||||
from external_media_tests.utils import verbose_until
|
||||
|
||||
|
||||
# Adapted from
|
||||
# https://github.com/gavinsharp/aboutmedia/blob/master/chrome/content/aboutmedia.xhtml
|
||||
debug_script = """
|
||||
var mainWindow = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
|
||||
.getInterface(Components.interfaces.nsIWebNavigation)
|
||||
.QueryInterface(Components.interfaces.nsIDocShellTreeItem)
|
||||
.rootTreeItem
|
||||
.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
|
||||
.getInterface(Components.interfaces.nsIDOMWindow);
|
||||
var tabbrowser = mainWindow.gBrowser;
|
||||
for (var i=0; i < tabbrowser.browsers.length; ++i) {
|
||||
var b = tabbrowser.getBrowserAtIndex(i);
|
||||
var media = b.contentDocumentAsCPOW.getElementsByTagName('video');
|
||||
for (var j=0; j < media.length; ++j) {
|
||||
var ms = media[j].mozMediaSourceObject;
|
||||
if (ms) {
|
||||
debugLines = ms.mozDebugReaderData.split(\"\\n\");
|
||||
return debugLines;
|
||||
}
|
||||
}
|
||||
}"""
|
||||
|
||||
|
||||
class VideoPuppeteer(object):
|
||||
"""
|
||||
Wrapper to control and introspect HTML5 video elements.
|
||||
|
||||
A note about properties like current_time and duration:
|
||||
These describe whatever stream is playing when the state is checked.
|
||||
It is possible that many different streams are dynamically spliced
|
||||
together, so the video stream that is currently playing might be the main
|
||||
video or it might be something else, like an ad, for example.
|
||||
|
||||
:param marionette: The marionette instance this runs in.
|
||||
:param url: the URL of the page containing the video element.
|
||||
:param video_selector: the selector of the element that we want to watch.
|
||||
This is set by default to 'video', which is what most sites use, but
|
||||
others should work.
|
||||
:param interval: The polling interval that is used to check progress.
|
||||
:param set_duration: When set to >0, the polling and checking will stop at
|
||||
the number of seconds specified. Otherwise, this will stop at the end
|
||||
of the video.
|
||||
:param stall_wait_time: The amount of time to wait to see if a stall has
|
||||
cleared. If 0, do not check for stalls.
|
||||
:param timeout: The amount of time to wait until the video starts.
|
||||
"""
|
||||
|
||||
_video_var_script = (
|
||||
'var video = arguments[0];'
|
||||
'var baseURI = arguments[0].baseURI;'
|
||||
'var currentTime = video.wrappedJSObject.currentTime;'
|
||||
'var duration = video.wrappedJSObject.duration;'
|
||||
'var buffered = video.wrappedJSObject.buffered;'
|
||||
'var bufferedRanges = [];'
|
||||
'for (var i = 0; i < buffered.length; i++) {'
|
||||
'bufferedRanges.push([buffered.start(i), buffered.end(i)]);'
|
||||
'}'
|
||||
'var played = video.wrappedJSObject.played;'
|
||||
'var playedRanges = [];'
|
||||
'for (var i = 0; i < played.length; i++) {'
|
||||
'playedRanges.push([played.start(i), played.end(i)]);'
|
||||
'}'
|
||||
'var totalFrames = '
|
||||
'video.getVideoPlaybackQuality()["totalVideoFrames"];'
|
||||
'var droppedFrames = '
|
||||
'video.getVideoPlaybackQuality()["droppedVideoFrames"];'
|
||||
'var corruptedFrames = '
|
||||
'video.getVideoPlaybackQuality()["corruptedVideoFrames"];'
|
||||
)
|
||||
"""
|
||||
A string containing JS that assigns video state to variables.
|
||||
The purpose of this string script is to be appended to by this and
|
||||
any inheriting classes to return these and other variables. In the case
|
||||
of an inheriting class the script can be added to in order to fetch
|
||||
further relevant variables -- keep in mind we only want one script
|
||||
execution to prevent races, so it wouldn't do to have child classes
|
||||
run this script then their own, as there is potential for lag in
|
||||
between.
|
||||
|
||||
This script assigns a subset of the vars used later by the
|
||||
`_video_state_named_tuple` function. Please see that function's
|
||||
documentation for further information on these variables.
|
||||
"""
|
||||
|
||||
def __init__(self, marionette, url, video_selector='video', interval=1,
|
||||
set_duration=0, stall_wait_time=0, timeout=60,
|
||||
autostart=True):
|
||||
self.marionette = marionette
|
||||
self.test_url = url
|
||||
self.interval = interval
|
||||
self.stall_wait_time = stall_wait_time
|
||||
self.timeout = timeout
|
||||
self._set_duration = set_duration
|
||||
self.video = None
|
||||
self.expected_duration = 0
|
||||
self._first_seen_time = 0
|
||||
self._first_seen_wall_time = 0
|
||||
self._fetch_state_script_string = None
|
||||
self._last_seen_video_state = None
|
||||
wait = Wait(self.marionette, timeout=self.timeout)
|
||||
with self.marionette.using_context(Marionette.CONTEXT_CONTENT):
|
||||
self.marionette.navigate(self.test_url)
|
||||
self.marionette.execute_script("""
|
||||
log('URL: {0}');""".format(self.test_url))
|
||||
verbose_until(wait, self,
|
||||
expected.element_present(By.TAG_NAME, 'video'))
|
||||
videos_found = self.marionette.find_elements(By.CSS_SELECTOR,
|
||||
video_selector)
|
||||
if len(videos_found) > 1:
|
||||
self.marionette.log(type(self).__name__ + ': multiple video '
|
||||
'elements found. '
|
||||
'Using first.')
|
||||
if len(videos_found) <= 0:
|
||||
self.marionette.log(type(self).__name__ + ': no video '
|
||||
'elements found.')
|
||||
return
|
||||
self.video = videos_found[0]
|
||||
self.marionette.execute_script("log('video element obtained');")
|
||||
if autostart:
|
||||
self.start()
|
||||
|
||||
def start(self):
|
||||
# To get an accurate expected_duration, playback must have started
|
||||
self._refresh_state()
|
||||
wait = Wait(self, timeout=self.timeout)
|
||||
verbose_until(wait, self, VideoPuppeteer.playback_started,
|
||||
"Check if video has played some range")
|
||||
self._first_seen_time = self._last_seen_video_state.current_time
|
||||
self._first_seen_wall_time = clock()
|
||||
self._update_expected_duration()
|
||||
|
||||
def get_debug_lines(self):
|
||||
"""
|
||||
Get Firefox internal debugging for the video element.
|
||||
|
||||
:return: A text string that has Firefox-internal debugging information.
|
||||
"""
|
||||
with self.marionette.using_context('chrome'):
|
||||
debug_lines = self.marionette.execute_script(debug_script)
|
||||
return debug_lines
|
||||
|
||||
def play(self):
|
||||
"""
|
||||
Tell the video element to Play.
|
||||
"""
|
||||
self._execute_video_script('arguments[0].wrappedJSObject.play();')
|
||||
|
||||
def pause(self):
|
||||
"""
|
||||
Tell the video element to Pause.
|
||||
"""
|
||||
self._execute_video_script('arguments[0].wrappedJSObject.pause();')
|
||||
|
||||
def playback_started(self):
|
||||
"""
|
||||
Determine if video has started
|
||||
|
||||
:param self: The VideoPuppeteer instance that we are interested in.
|
||||
|
||||
:return: True if is playing; False otherwise
|
||||
"""
|
||||
self._refresh_state()
|
||||
try:
|
||||
played_ranges = self._last_seen_video_state.played
|
||||
return (
|
||||
played_ranges.length > 0 and
|
||||
played_ranges.start(0) < played_ranges.end(0) and
|
||||
played_ranges.end(0) > 0.0
|
||||
)
|
||||
except Exception as e:
|
||||
print ('Got exception {}'.format(e))
|
||||
return False
|
||||
|
||||
def playback_done(self):
|
||||
"""
|
||||
If we are near the end and there is still a video element, then
|
||||
we are essentially done. If this happens to be last time we are polled
|
||||
before the video ends, we won't get another chance.
|
||||
|
||||
:param self: The VideoPuppeteer instance that we are interested in.
|
||||
|
||||
:return: True if we are close enough to the end of playback; False
|
||||
otherwise.
|
||||
"""
|
||||
self._refresh_state()
|
||||
|
||||
if self._last_seen_video_state.remaining_time < self.interval:
|
||||
return True
|
||||
|
||||
# Check to see if the video has stalled. Accumulate the amount of lag
|
||||
# since the video started, and if it is too high, then raise.
|
||||
if (self.stall_wait_time and
|
||||
self._last_seen_video_state.lag > self.stall_wait_time):
|
||||
raise VideoException('Video {} stalled.\n{}'
|
||||
.format(self._last_seen_video_state.video_uri,
|
||||
self))
|
||||
|
||||
# We are cruising, so we are not done.
|
||||
return False
|
||||
|
||||
def _update_expected_duration(self):
|
||||
"""
|
||||
Update the duration of the target video at self.test_url (in seconds).
|
||||
This is based on the last seen state, so the state should be,
|
||||
refreshed at least once before this is called.
|
||||
|
||||
expected_duration represents the following: how long do we expect
|
||||
playback to last before we consider the video to be 'complete'?
|
||||
If we only want to play the first n seconds of the video,
|
||||
expected_duration is set to n.
|
||||
"""
|
||||
|
||||
# self._last_seen_video_state.duration is the duration of whatever was
|
||||
# playing when the state was checked. In this case, we assume the video
|
||||
# element always shows the same stream throughout playback (i.e. the
|
||||
# are no ads spliced into the main video, for example), so
|
||||
# self._last_seen_video_state.duration is the duration of the main
|
||||
# video.
|
||||
video_duration = self._last_seen_video_state.duration
|
||||
# Do our best to figure out where the video started playing
|
||||
played_ranges = self._last_seen_video_state.played
|
||||
if played_ranges.length > 0:
|
||||
# If we have a range we should only have on continuous range
|
||||
assert played_ranges.length == 1
|
||||
start_position = played_ranges.start(0)
|
||||
else:
|
||||
# If we don't have a range we should have a current time
|
||||
start_position = self._first_seen_time
|
||||
# In case video starts at t > 0, adjust target time partial playback
|
||||
remaining_video = video_duration - start_position
|
||||
if 0 < self._set_duration < remaining_video:
|
||||
self.expected_duration = self._set_duration
|
||||
else:
|
||||
self.expected_duration = remaining_video
|
||||
|
||||
@staticmethod
|
||||
def _video_state_named_tuple():
|
||||
"""
|
||||
Create a named tuple class that can be used to store state snapshots
|
||||
of the wrapped element. The fields in the tuple should be used as
|
||||
follows:
|
||||
|
||||
base_uri: the baseURI attribute of the wrapped element.
|
||||
current_time: the current time of the wrapped element.
|
||||
duration: the duration of the wrapped element.
|
||||
buffered: the buffered ranges of the wrapped element. In its raw form
|
||||
this is as a list where the first element is the length and the second
|
||||
element is a list of 2 item lists, where each two items are a buffered
|
||||
range. Once assigned to the tuple this data should be wrapped in the
|
||||
TimeRanges class.
|
||||
played: the played ranges of the wrapped element. In its raw form this
|
||||
is as a list where the first element is the length and the second
|
||||
element is a list of 2 item lists, where each two items are a played
|
||||
range. Once assigned to the tuple this data should be wrapped in the
|
||||
TimeRanges class.
|
||||
lag: the difference in real world time and wrapped element time.
|
||||
Calculated as real world time passed - element time passed.
|
||||
totalFrames: number of total frames for the wrapped element
|
||||
droppedFrames: number of dropped frames for the wrapped element.
|
||||
corruptedFrames: number of corrupted frames for the wrapped.
|
||||
video_src: the src attribute of the wrapped element.
|
||||
|
||||
:return: A 'video_state_info' named tuple class.
|
||||
"""
|
||||
return namedtuple('video_state_info',
|
||||
['base_uri',
|
||||
'current_time',
|
||||
'duration',
|
||||
'remaining_time',
|
||||
'buffered',
|
||||
'played',
|
||||
'lag',
|
||||
'total_frames',
|
||||
'dropped_frames',
|
||||
'corrupted_frames',
|
||||
'video_src'])
|
||||
|
||||
def _create_video_state_info(self, **video_state_info_kwargs):
|
||||
"""
|
||||
Create an instance of the video_state_info named tuple. This function
|
||||
expects a dictionary populated with the following keys: current_time,
|
||||
duration, raw_played_ranges, total_frames, dropped_frames, and
|
||||
corrupted_frames.
|
||||
|
||||
Aside from raw_played_ranges, see `_video_state_named_tuple` for more
|
||||
information on the above keys and values. For raw_played_ranges a
|
||||
list is expected that can be consumed to make a TimeRanges object.
|
||||
|
||||
:return: A named tuple 'video_state_info' derived from arguments and
|
||||
state information from the puppeteer.
|
||||
"""
|
||||
raw_buffered_ranges = video_state_info_kwargs['raw_buffered_ranges']
|
||||
raw_played_ranges = video_state_info_kwargs['raw_played_ranges']
|
||||
# Remove raw ranges from dict as they are not used in the final named
|
||||
# tuple and will provide an unexpected kwarg if kept.
|
||||
del video_state_info_kwargs['raw_buffered_ranges']
|
||||
del video_state_info_kwargs['raw_played_ranges']
|
||||
# Create buffered ranges
|
||||
video_state_info_kwargs['buffered'] = (
|
||||
TimeRanges(raw_buffered_ranges[0], raw_buffered_ranges[1]))
|
||||
# Create played ranges
|
||||
video_state_info_kwargs['played'] = (
|
||||
TimeRanges(raw_played_ranges[0], raw_played_ranges[1]))
|
||||
# Calculate elapsed times
|
||||
elapsed_current_time = (video_state_info_kwargs['current_time'] -
|
||||
self._first_seen_time)
|
||||
elapsed_wall_time = clock() - self._first_seen_wall_time
|
||||
# Calculate lag
|
||||
video_state_info_kwargs['lag'] = (
|
||||
elapsed_wall_time - elapsed_current_time)
|
||||
# Calculate remaining time
|
||||
if video_state_info_kwargs['played'].length > 0:
|
||||
played_duration = (video_state_info_kwargs['played'].end(0) -
|
||||
video_state_info_kwargs['played'].start(0))
|
||||
video_state_info_kwargs['remaining_time'] = (
|
||||
self.expected_duration - played_duration)
|
||||
else:
|
||||
# No playback has happened yet, remaining time is duration
|
||||
video_state_info_kwargs['remaining_time'] = self.expected_duration
|
||||
# Fetch non time critical source information
|
||||
video_state_info_kwargs['video_src'] = self.video.get_attribute('src')
|
||||
# Create video state snapshot
|
||||
state_info = self._video_state_named_tuple()
|
||||
return state_info(**video_state_info_kwargs)
|
||||
|
||||
@property
|
||||
def _fetch_state_script(self):
|
||||
if not self._fetch_state_script_string:
|
||||
self._fetch_state_script_string = (
|
||||
self._video_var_script +
|
||||
'return ['
|
||||
'baseURI,'
|
||||
'currentTime,'
|
||||
'duration,'
|
||||
'[buffered.length, bufferedRanges],'
|
||||
'[played.length, playedRanges],'
|
||||
'totalFrames,'
|
||||
'droppedFrames,'
|
||||
'corruptedFrames];')
|
||||
return self._fetch_state_script_string
|
||||
|
||||
def _refresh_state(self):
|
||||
"""
|
||||
Refresh the snapshot of the underlying video state. We do this all
|
||||
in one so that the state doesn't change in between queries.
|
||||
|
||||
We also store information that can be derived from the snapshotted
|
||||
information, such as lag. This is stored in the last seen state to
|
||||
stress that it's based on the snapshot.
|
||||
"""
|
||||
keys = ['base_uri', 'current_time', 'duration', 'raw_buffered_ranges',
|
||||
'raw_played_ranges', 'total_frames', 'dropped_frames',
|
||||
'corrupted_frames']
|
||||
values = self._execute_video_script(self._fetch_state_script)
|
||||
self._last_seen_video_state = (
|
||||
self._create_video_state_info(**dict(zip(keys, values))))
|
||||
|
||||
def _measure_progress(self):
|
||||
self._refresh_state()
|
||||
initial = self._last_seen_video_state.current_time
|
||||
sleep(1)
|
||||
self._refresh_state()
|
||||
return self._last_seen_video_state.current_time - initial
|
||||
|
||||
def _execute_video_script(self, script):
|
||||
"""
|
||||
Execute JS script in content context with access to video element.
|
||||
|
||||
:param script: script to be executed
|
||||
:return: value returned by script
|
||||
"""
|
||||
with self.marionette.using_context(Marionette.CONTEXT_CONTENT):
|
||||
return self.marionette.execute_script(script,
|
||||
script_args=[self.video])
|
||||
|
||||
def __str__(self):
|
||||
messages = ['{} - test url: {}: '
|
||||
.format(type(self).__name__, self.test_url)]
|
||||
if not self.video:
|
||||
messages += ['\tvideo: None']
|
||||
return '\n'.join(messages)
|
||||
if not self._last_seen_video_state:
|
||||
messages += ['\tvideo: No last seen state']
|
||||
return '\n'.join(messages)
|
||||
# Have video and state info
|
||||
messages += [
|
||||
'{',
|
||||
'\t(video)'
|
||||
]
|
||||
messages += ['\tinterval: {}'.format(self.interval)]
|
||||
messages += ['\texpected duration: {}'.format(self.expected_duration)]
|
||||
messages += ['\tstall wait time: {}'.format(self.stall_wait_time)]
|
||||
messages += ['\ttimeout: {}'.format(self.timeout)]
|
||||
# Print each field on its own line
|
||||
for field in self._last_seen_video_state._fields:
|
||||
# For compatibility with different test environments we force ascii
|
||||
field_ascii = (
|
||||
unicode(getattr(self._last_seen_video_state, field))
|
||||
.encode('ascii','replace'))
|
||||
messages += [('\t{}: {}'.format(field, field_ascii))]
|
||||
messages += '}'
|
||||
return '\n'.join(messages)
|
||||
|
||||
|
||||
class VideoException(Exception):
|
||||
"""
|
||||
Exception class to use for video-specific error processing.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class TimeRanges:
|
||||
"""
|
||||
Class to represent the TimeRanges data returned by played(). Exposes a
|
||||
similar interface to the JavaScript TimeRanges object.
|
||||
"""
|
||||
def __init__(self, length, ranges):
|
||||
# These should be the same,. Theoretically we don't need the length,
|
||||
# but since this should be used to consume data coming back from
|
||||
# JS exec, this is a valid sanity check.
|
||||
assert length == len(ranges)
|
||||
self.length = length
|
||||
self.ranges = [(pair[0], pair[1]) for pair in ranges]
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
'TimeRanges: length: {}, ranges: {}'
|
||||
.format(self.length, self.ranges)
|
||||
)
|
||||
|
||||
def start(self, index):
|
||||
return self.ranges[index][0]
|
||||
|
||||
def end(self, index):
|
||||
return self.ranges[index][1]
|
||||
496
dom/media/test/external/external_media_tests/media_utils/youtube_puppeteer.py
vendored
Normal file
496
dom/media/test/external/external_media_tests/media_utils/youtube_puppeteer.py
vendored
Normal file
|
|
@ -0,0 +1,496 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
import re
|
||||
|
||||
from collections import namedtuple
|
||||
from json import loads
|
||||
from time import sleep
|
||||
|
||||
from marionette_driver import By, expected, Wait
|
||||
from marionette_driver.errors import TimeoutException, NoSuchElementException
|
||||
from marionette_harness import Marionette
|
||||
|
||||
from video_puppeteer import VideoPuppeteer, VideoException
|
||||
from external_media_tests.utils import verbose_until
|
||||
|
||||
|
||||
class YouTubePuppeteer(VideoPuppeteer):
|
||||
"""
|
||||
Wrapper around a YouTube .html5-video-player element.
|
||||
|
||||
Can be used with youtube videos or youtube videos at embedded URLS. E.g.
|
||||
both https://www.youtube.com/watch?v=AbAACm1IQE0 and
|
||||
https://www.youtube.com/embed/AbAACm1IQE0 should work.
|
||||
|
||||
Using an embedded video has the advantage of not auto-playing more videos
|
||||
while a test is running.
|
||||
|
||||
Compared to video puppeteer, this class has the benefit of accessing the
|
||||
youtube player object as well as the video element. The YT player will
|
||||
report information for the underlying video even if an add is playing (as
|
||||
opposed to the video element behaviour, which will report on whatever
|
||||
is play at the time of query), and can also report if an ad is playing.
|
||||
|
||||
Partial reference: https://developers.google.com/youtube/iframe_api_reference.
|
||||
This reference is useful for site-specific features such as interacting
|
||||
with ads, or accessing YouTube's debug data.
|
||||
"""
|
||||
|
||||
_player_var_script = (
|
||||
'var player_duration = arguments[1].wrappedJSObject.getDuration();'
|
||||
'var player_current_time = '
|
||||
'arguments[1].wrappedJSObject.getCurrentTime();'
|
||||
'var player_playback_quality = '
|
||||
'arguments[1].wrappedJSObject.getPlaybackQuality();'
|
||||
'var player_movie_id = '
|
||||
'arguments[1].wrappedJSObject.getVideoData()["video_id"];'
|
||||
'var player_movie_title = '
|
||||
'arguments[1].wrappedJSObject.getVideoData()["title"];'
|
||||
'var player_url = '
|
||||
'arguments[1].wrappedJSObject.getVideoUrl();'
|
||||
'var player_state = '
|
||||
'arguments[1].wrappedJSObject.getPlayerState();'
|
||||
'var player_ad_state = arguments[1].wrappedJSObject.getAdState();'
|
||||
'var player_breaks_count = '
|
||||
'arguments[1].wrappedJSObject.getOption("ad", "breakscount");'
|
||||
)
|
||||
"""
|
||||
A string containing JS that will assign player state to
|
||||
variables. This is similar to `_video_var_script` from
|
||||
`VideoPuppeteer`. See `_video_var_script` for more information on the
|
||||
motivation for this method.
|
||||
|
||||
This script assigns a subset of the vars used later by the
|
||||
`_yt_state_named_tuple` function. Please see that functions
|
||||
documentation for further information on these variables.
|
||||
"""
|
||||
|
||||
_yt_player_state = {
|
||||
'UNSTARTED': -1,
|
||||
'ENDED': 0,
|
||||
'PLAYING': 1,
|
||||
'PAUSED': 2,
|
||||
'BUFFERING': 3,
|
||||
'CUED': 5
|
||||
}
|
||||
_yt_player_state_name = {v: k for k, v in _yt_player_state.items()}
|
||||
_time_pattern = re.compile('(?P<minute>\d+):(?P<second>\d+)')
|
||||
|
||||
def __init__(self, marionette, url, autostart=True, **kwargs):
|
||||
self.player = None
|
||||
self._last_seen_player_state = None
|
||||
super(YouTubePuppeteer,
|
||||
self).__init__(marionette, url,
|
||||
video_selector='.html5-video-player video',
|
||||
autostart=False,
|
||||
**kwargs)
|
||||
wait = Wait(self.marionette, timeout=30)
|
||||
with self.marionette.using_context(Marionette.CONTEXT_CONTENT):
|
||||
verbose_until(wait, self,
|
||||
expected.element_present(By.CLASS_NAME,
|
||||
'html5-video-player'))
|
||||
self.player = self.marionette.find_element(By.CLASS_NAME,
|
||||
'html5-video-player')
|
||||
self.marionette.execute_script("log('.html5-video-player "
|
||||
"element obtained');")
|
||||
# When an ad is playing, self.player_duration indicates the duration
|
||||
# of the spliced-in ad stream, not the duration of the main video, so
|
||||
# we attempt to skip the ad first.
|
||||
for attempt in range(5):
|
||||
sleep(1)
|
||||
self.process_ad()
|
||||
if (self._last_seen_player_state.player_ad_inactive and
|
||||
self._last_seen_video_state.duration and not
|
||||
self._last_seen_player_state.player_buffering):
|
||||
break
|
||||
self._update_expected_duration()
|
||||
if autostart:
|
||||
self.start()
|
||||
|
||||
def player_play(self):
|
||||
"""
|
||||
Play via YouTube API.
|
||||
"""
|
||||
self._execute_yt_script('arguments[1].wrappedJSObject.playVideo();')
|
||||
|
||||
def player_pause(self):
|
||||
"""
|
||||
Pause via YouTube API.
|
||||
"""
|
||||
self._execute_yt_script('arguments[1].wrappedJSObject.pauseVideo();')
|
||||
|
||||
def _player_measure_progress(self):
|
||||
"""
|
||||
Determine player progress. Refreshes state.
|
||||
|
||||
:return: Playback progress in seconds via YouTube API with snapshots.
|
||||
"""
|
||||
self._refresh_state()
|
||||
initial = self._last_seen_player_state.player_current_time
|
||||
sleep(1)
|
||||
self._refresh_state()
|
||||
return self._last_seen_player_state.player_current_time - initial
|
||||
|
||||
def _get_player_debug_dict(self):
|
||||
text = self._execute_yt_script('return arguments[1].'
|
||||
'wrappedJSObject.getDebugText();')
|
||||
if text:
|
||||
try:
|
||||
return loads(text)
|
||||
except ValueError:
|
||||
self.marionette.log('Error loading json: DebugText',
|
||||
level='DEBUG')
|
||||
|
||||
def _execute_yt_script(self, script):
|
||||
"""
|
||||
Execute JS script in content context with access to video element and
|
||||
YouTube .html5-video-player element.
|
||||
|
||||
:param script: script to be executed.
|
||||
|
||||
:return: value returned by script
|
||||
"""
|
||||
with self.marionette.using_context(Marionette.CONTEXT_CONTENT):
|
||||
return self.marionette.execute_script(script,
|
||||
script_args=[self.video,
|
||||
self.player])
|
||||
|
||||
def _check_if_ad_ended(self):
|
||||
self._refresh_state()
|
||||
return self._last_seen_player_state.player_ad_ended
|
||||
|
||||
def process_ad(self):
|
||||
"""
|
||||
Wait for this ad to finish. Refreshes state.
|
||||
"""
|
||||
self._refresh_state()
|
||||
if self._last_seen_player_state.player_ad_inactive:
|
||||
return
|
||||
ad_timeout = (self._search_ad_duration() or 30) + 5
|
||||
wait = Wait(self, timeout=ad_timeout, interval=1)
|
||||
try:
|
||||
self.marionette.log('process_ad: waiting {} s for ad'
|
||||
.format(ad_timeout))
|
||||
verbose_until(wait,
|
||||
self,
|
||||
YouTubePuppeteer._check_if_ad_ended,
|
||||
"Check if ad ended")
|
||||
except TimeoutException:
|
||||
self.marionette.log('Waiting for ad to end timed out',
|
||||
level='WARNING')
|
||||
|
||||
def _search_ad_duration(self):
|
||||
"""
|
||||
Try and determine ad duration. Refreshes state.
|
||||
|
||||
:return: ad duration in seconds, if currently displayed in player
|
||||
"""
|
||||
self._refresh_state()
|
||||
if not (self._last_seen_player_state.player_ad_playing or
|
||||
self._player_measure_progress() == 0):
|
||||
return None
|
||||
if (self._last_seen_player_state.player_ad_playing and
|
||||
self._last_seen_video_state.duration):
|
||||
return self._last_seen_video_state.duration
|
||||
selector = '.html5-video-player .videoAdUiAttribution'
|
||||
wait = Wait(self.marionette, timeout=5)
|
||||
try:
|
||||
with self.marionette.using_context(Marionette.CONTEXT_CONTENT):
|
||||
wait.until(expected.element_present(By.CSS_SELECTOR,
|
||||
selector))
|
||||
countdown = self.marionette.find_element(By.CSS_SELECTOR,
|
||||
selector)
|
||||
ad_time = self._time_pattern.search(countdown.text)
|
||||
if ad_time:
|
||||
ad_minutes = int(ad_time.group('minute'))
|
||||
ad_seconds = int(ad_time.group('second'))
|
||||
return 60 * ad_minutes + ad_seconds
|
||||
except (TimeoutException, NoSuchElementException):
|
||||
self.marionette.log('Could not obtain '
|
||||
'element: {}'.format(selector),
|
||||
level='WARNING')
|
||||
return None
|
||||
|
||||
def _player_stalled(self):
|
||||
"""
|
||||
Checks if the player has stalled. Refreshes state.
|
||||
|
||||
:return: True if playback is not making progress for 4-9 seconds. This
|
||||
excludes ad breaks. Note that the player might just be busy with
|
||||
buffering due to a slow network.
|
||||
"""
|
||||
|
||||
# `current_time` stands still while ad is playing
|
||||
def condition():
|
||||
# no ad is playing and current_time stands still
|
||||
return (not self._last_seen_player_state.player_ad_playing and
|
||||
self._measure_progress() < 0.1 and
|
||||
self._player_measure_progress() < 0.1 and
|
||||
(self._last_seen_player_state.player_playing or
|
||||
self._last_seen_player_state.player_buffering))
|
||||
|
||||
if condition():
|
||||
sleep(2)
|
||||
self._refresh_state()
|
||||
if self._last_seen_player_state.player_buffering:
|
||||
sleep(5)
|
||||
self._refresh_state()
|
||||
return condition()
|
||||
else:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _yt_state_named_tuple():
|
||||
"""
|
||||
Create a named tuple class that can be used to store state snapshots
|
||||
of the wrapped youtube player. The fields in the tuple should be used
|
||||
as follows:
|
||||
|
||||
player_duration: the duration as fetched from the wrapped player.
|
||||
player_current_time: the current playback time as fetched from the
|
||||
wrapped player.
|
||||
player_remaining_time: the remaining time as calculated based on the
|
||||
puppeteers expected time and the players current time.
|
||||
player_playback_quality: the playback quality as fetched from the
|
||||
wrapped player. See:
|
||||
https://developers.google.com/youtube/js_api_reference#Playback_quality
|
||||
player_movie_id: the movie id fetched from the wrapped player.
|
||||
player_movie_title: the title fetched from the wrapped player.
|
||||
player_url: the self reported url fetched from the wrapped player.
|
||||
player_state: the current state of playback as fetch from the wrapped
|
||||
player. See:
|
||||
https://developers.google.com/youtube/js_api_reference#Playback_status
|
||||
player_unstarted, player_ended, player_playing, player_paused,
|
||||
player_buffering, and player_cued: these are all shortcuts to the
|
||||
player state, only one should be true at any time.
|
||||
player_ad_state: as player_state, but reports for the current ad.
|
||||
player_ad_state, player_ad_inactive, player_ad_playing, and
|
||||
player_ad_ended: these are all shortcuts to the ad state, only one
|
||||
should be true at any time.
|
||||
player_breaks_count: the number of ads as fetched from the wrapped
|
||||
player. This includes both played and unplayed ads, and includes
|
||||
streaming ads as well as pop up ads.
|
||||
|
||||
:return: A 'player_state_info' named tuple class.
|
||||
"""
|
||||
return namedtuple('player_state_info',
|
||||
['player_duration',
|
||||
'player_current_time',
|
||||
'player_remaining_time',
|
||||
'player_playback_quality',
|
||||
'player_movie_id',
|
||||
'player_movie_title',
|
||||
'player_url',
|
||||
'player_state',
|
||||
'player_unstarted',
|
||||
'player_ended',
|
||||
'player_playing',
|
||||
'player_paused',
|
||||
'player_buffering',
|
||||
'player_cued',
|
||||
'player_ad_state',
|
||||
'player_ad_inactive',
|
||||
'player_ad_playing',
|
||||
'player_ad_ended',
|
||||
'player_breaks_count'
|
||||
])
|
||||
|
||||
def _create_player_state_info(self, **player_state_info_kwargs):
|
||||
"""
|
||||
Create an instance of the state info named tuple. This function
|
||||
expects a dictionary containing the following keys:
|
||||
player_duration, player_current_time, player_playback_quality,
|
||||
player_movie_id, player_movie_title, player_url, player_state,
|
||||
player_ad_state, and player_breaks_count.
|
||||
|
||||
For more information on the above keys and their values see
|
||||
`_yt_state_named_tuple`.
|
||||
|
||||
:return: A named tuple 'yt_state_info', derived from arguments and
|
||||
state information from the puppeteer.
|
||||
"""
|
||||
player_state_info_kwargs['player_remaining_time'] = (
|
||||
self.expected_duration -
|
||||
player_state_info_kwargs['player_current_time'])
|
||||
# Calculate player state convenience info
|
||||
player_state = player_state_info_kwargs['player_state']
|
||||
player_state_info_kwargs['player_unstarted'] = (
|
||||
player_state == self._yt_player_state['UNSTARTED'])
|
||||
player_state_info_kwargs['player_ended'] = (
|
||||
player_state == self._yt_player_state['ENDED'])
|
||||
player_state_info_kwargs['player_playing'] = (
|
||||
player_state == self._yt_player_state['PLAYING'])
|
||||
player_state_info_kwargs['player_paused'] = (
|
||||
player_state == self._yt_player_state['PAUSED'])
|
||||
player_state_info_kwargs['player_buffering'] = (
|
||||
player_state == self._yt_player_state['BUFFERING'])
|
||||
player_state_info_kwargs['player_cued'] = (
|
||||
player_state == self._yt_player_state['CUED'])
|
||||
# Calculate ad state convenience info
|
||||
player_ad_state = player_state_info_kwargs['player_ad_state']
|
||||
player_state_info_kwargs['player_ad_inactive'] = (
|
||||
player_ad_state == self._yt_player_state['UNSTARTED'])
|
||||
player_state_info_kwargs['player_ad_playing'] = (
|
||||
player_ad_state == self._yt_player_state['PLAYING'])
|
||||
player_state_info_kwargs['player_ad_ended'] = (
|
||||
player_ad_state == self._yt_player_state['ENDED'])
|
||||
# Create player snapshot
|
||||
state_info = self._yt_state_named_tuple()
|
||||
return state_info(**player_state_info_kwargs)
|
||||
|
||||
@property
|
||||
def _fetch_state_script(self):
|
||||
if not self._fetch_state_script_string:
|
||||
self._fetch_state_script_string = (
|
||||
self._video_var_script +
|
||||
self._player_var_script +
|
||||
'return ['
|
||||
'baseURI,'
|
||||
'currentTime,'
|
||||
'duration,'
|
||||
'[buffered.length, bufferedRanges],'
|
||||
'[played.length, playedRanges],'
|
||||
'totalFrames,'
|
||||
'droppedFrames,'
|
||||
'corruptedFrames,'
|
||||
'player_duration,'
|
||||
'player_current_time,'
|
||||
'player_playback_quality,'
|
||||
'player_movie_id,'
|
||||
'player_movie_title,'
|
||||
'player_url,'
|
||||
'player_state,'
|
||||
'player_ad_state,'
|
||||
'player_breaks_count];')
|
||||
return self._fetch_state_script_string
|
||||
|
||||
def _refresh_state(self):
|
||||
"""
|
||||
Refresh the snapshot of the underlying video and player state. We do
|
||||
this allin one so that the state doesn't change in between queries.
|
||||
|
||||
We also store information that can be derived from the snapshotted
|
||||
information, such as lag. This is stored in the last seen state to
|
||||
stress that it's based on the snapshot.
|
||||
"""
|
||||
values = self._execute_yt_script(self._fetch_state_script)
|
||||
video_keys = ['base_uri', 'current_time', 'duration',
|
||||
'raw_buffered_ranges', 'raw_played_ranges',
|
||||
'total_frames', 'dropped_frames', 'corrupted_frames']
|
||||
player_keys = ['player_duration', 'player_current_time',
|
||||
'player_playback_quality', 'player_movie_id',
|
||||
'player_movie_title', 'player_url', 'player_state',
|
||||
'player_ad_state', 'player_breaks_count']
|
||||
# Get video state
|
||||
self._last_seen_video_state = (
|
||||
self._create_video_state_info(**dict(
|
||||
zip(video_keys, values[:len(video_keys)]))))
|
||||
# Get player state
|
||||
self._last_seen_player_state = (
|
||||
self._create_player_state_info(**dict(
|
||||
zip(player_keys, values[-len(player_keys):]))))
|
||||
|
||||
def mse_enabled(self):
|
||||
"""
|
||||
Check if the video source indicates mse usage for current video.
|
||||
Refreshes state.
|
||||
|
||||
:return: True if MSE is being used, False if not.
|
||||
"""
|
||||
self._refresh_state()
|
||||
return self._last_seen_video_state.video_src.startswith('blob')
|
||||
|
||||
def playback_started(self):
|
||||
"""
|
||||
Check whether playback has started. Refreshes state.
|
||||
|
||||
:return: True if play back has started, False if not.
|
||||
"""
|
||||
self._refresh_state()
|
||||
# usually, ad is playing during initial buffering
|
||||
if (self._last_seen_player_state.player_playing or
|
||||
self._last_seen_player_state.player_buffering):
|
||||
return True
|
||||
if (self._last_seen_video_state.current_time > 0 or
|
||||
self._last_seen_player_state.player_current_time > 0):
|
||||
return True
|
||||
return False
|
||||
|
||||
def playback_done(self):
|
||||
"""
|
||||
Check whether playback is done. Refreshes state.
|
||||
|
||||
:return: True if play back has ended, False if not.
|
||||
"""
|
||||
# in case ad plays at end of video
|
||||
self._refresh_state()
|
||||
if self._last_seen_player_state.player_ad_playing:
|
||||
return False
|
||||
return (self._last_seen_player_state.player_ended or
|
||||
self._last_seen_player_state.player_remaining_time < 1)
|
||||
|
||||
def wait_for_almost_done(self, final_piece=120):
|
||||
"""
|
||||
Allow the given video to play until only `final_piece` seconds remain,
|
||||
skipping ads mid-way as much as possible.
|
||||
`final_piece` should be short enough to not be interrupted by an ad.
|
||||
|
||||
Depending on the length of the video, check the ad status every 10-30
|
||||
seconds, skip an active ad if possible.
|
||||
|
||||
This call refreshes state.
|
||||
|
||||
:param final_piece: The length in seconds of the desired remaining time
|
||||
to wait until.
|
||||
"""
|
||||
self._refresh_state()
|
||||
rest = 10
|
||||
duration = remaining_time = self.expected_duration
|
||||
if duration < final_piece:
|
||||
# video is short so don't attempt to skip more ads
|
||||
return duration
|
||||
elif duration > 600:
|
||||
# for videos that are longer than 10 minutes
|
||||
# wait longer between checks
|
||||
rest = duration / 50
|
||||
|
||||
while remaining_time > final_piece:
|
||||
if self._player_stalled():
|
||||
if self._last_seen_player_state.player_buffering:
|
||||
# fall back on timeout in 'wait' call that comes after this
|
||||
# in test function
|
||||
self.marionette.log('Buffering and no playback progress.')
|
||||
break
|
||||
else:
|
||||
message = '\n'.join(['Playback stalled', str(self)])
|
||||
raise VideoException(message)
|
||||
if self._last_seen_player_state.player_breaks_count > 0:
|
||||
self.process_ad()
|
||||
if remaining_time > 1.5 * rest:
|
||||
sleep(rest)
|
||||
else:
|
||||
sleep(rest / 2)
|
||||
# TODO during an ad, remaining_time will be based on ad's current_time
|
||||
# rather than current_time of target video
|
||||
remaining_time = self._last_seen_player_state.player_remaining_time
|
||||
return remaining_time
|
||||
|
||||
def __str__(self):
|
||||
messages = [super(YouTubePuppeteer, self).__str__()]
|
||||
if not self.player:
|
||||
messages += ['\t.html5-media-player: None']
|
||||
return '\n'.join(messages)
|
||||
if not self._last_seen_player_state:
|
||||
messages += ['\t.html5-media-player: No last seen state']
|
||||
return '\n'.join(messages)
|
||||
messages += ['.html5-media-player: {']
|
||||
for field in self._last_seen_player_state._fields:
|
||||
# For compatibility with different test environments we force ascii
|
||||
field_ascii = (
|
||||
unicode(getattr(self._last_seen_player_state, field))
|
||||
.encode('ascii', 'replace'))
|
||||
messages += [('\t{}: {}'.format(field, field_ascii))]
|
||||
messages += '}'
|
||||
return '\n'.join(messages)
|
||||
Loading…
Add table
Add a link
Reference in a new issue