summaryrefslogtreecommitdiffstats
path: root/dom/media/test/external/external_media_tests
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /dom/media/test/external/external_media_tests
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'dom/media/test/external/external_media_tests')
-rw-r--r--dom/media/test/external/external_media_tests/__init__.py10
-rw-r--r--dom/media/test/external/external_media_tests/manifest.ini1
-rw-r--r--dom/media/test/external/external_media_tests/media_utils/__init__.py0
-rw-r--r--dom/media/test/external/external_media_tests/media_utils/video_puppeteer.py448
-rw-r--r--dom/media/test/external/external_media_tests/media_utils/youtube_puppeteer.py496
-rw-r--r--dom/media/test/external/external_media_tests/playback/eme.ini1
-rw-r--r--dom/media/test/external/external_media_tests/playback/limiting_bandwidth.ini2
-rw-r--r--dom/media/test/external/external_media_tests/playback/manifest.ini1
-rw-r--r--dom/media/test/external/external_media_tests/playback/netflix_limiting_bandwidth.ini1
-rw-r--r--dom/media/test/external/external_media_tests/playback/test_eme_playback.py18
-rw-r--r--dom/media/test/external/external_media_tests/playback/test_eme_playback_limiting_bandwidth.py24
-rw-r--r--dom/media/test/external/external_media_tests/playback/test_full_playback.py25
-rw-r--r--dom/media/test/external/external_media_tests/playback/test_playback_limiting_bandwidth.py17
-rw-r--r--dom/media/test/external/external_media_tests/playback/test_shaka_playback.py42
-rw-r--r--dom/media/test/external/external_media_tests/playback/test_ultra_low_bandwidth.py15
-rw-r--r--dom/media/test/external/external_media_tests/playback/test_video_playback.py15
-rw-r--r--dom/media/test/external/external_media_tests/playback/youtube/manifest.ini1
-rw-r--r--dom/media/test/external/external_media_tests/playback/youtube/test_basic_playback.py74
-rw-r--r--dom/media/test/external/external_media_tests/playback/youtube/test_prefs.py46
-rw-r--r--dom/media/test/external/external_media_tests/resources/mozilla.html45
-rw-r--r--dom/media/test/external/external_media_tests/test_example.py21
-rw-r--r--dom/media/test/external/external_media_tests/urls/default.ini9
-rw-r--r--dom/media/test/external/external_media_tests/urls/netflix/default.ini8
-rw-r--r--dom/media/test/external/external_media_tests/urls/shaka-player/default.ini30
-rw-r--r--dom/media/test/external/external_media_tests/urls/youtube/archive/crash_videos.ini25
-rw-r--r--dom/media/test/external/external_media_tests/urls/youtube/archive/other_videos.ini19
-rw-r--r--dom/media/test/external/external_media_tests/urls/youtube/archive/video_data.ini21
-rw-r--r--dom/media/test/external/external_media_tests/urls/youtube/archive/youtube.ini38
-rw-r--r--dom/media/test/external/external_media_tests/urls/youtube/long1-720.ini5
-rw-r--r--dom/media/test/external/external_media_tests/urls/youtube/long2-crashes-720.ini39
-rw-r--r--dom/media/test/external/external_media_tests/urls/youtube/long3-crashes-900.ini86
-rw-r--r--dom/media/test/external/external_media_tests/urls/youtube/medium1-60.ini18
-rw-r--r--dom/media/test/external/external_media_tests/urls/youtube/short1-10.ini13
-rw-r--r--dom/media/test/external/external_media_tests/urls/youtube/short2-crashes-15.ini17
-rw-r--r--dom/media/test/external/external_media_tests/utils.py68
35 files changed, 1699 insertions, 0 deletions
diff --git a/dom/media/test/external/external_media_tests/__init__.py b/dom/media/test/external/external_media_tests/__init__.py
new file mode 100644
index 000000000..bf7ceec47
--- /dev/null
+++ b/dom/media/test/external/external_media_tests/__init__.py
@@ -0,0 +1,10 @@
+# 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 os
+
+root = os.path.abspath(os.path.dirname(__file__))
+manifest = os.path.join(root, 'manifest.ini')
+resources = os.path.join(root, 'resources')
+urls = os.path.join(root, 'urls')
diff --git a/dom/media/test/external/external_media_tests/manifest.ini b/dom/media/test/external/external_media_tests/manifest.ini
new file mode 100644
index 000000000..e370fd679
--- /dev/null
+++ b/dom/media/test/external/external_media_tests/manifest.ini
@@ -0,0 +1 @@
+[include:playback/manifest.ini]
diff --git a/dom/media/test/external/external_media_tests/media_utils/__init__.py b/dom/media/test/external/external_media_tests/media_utils/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/dom/media/test/external/external_media_tests/media_utils/__init__.py
diff --git a/dom/media/test/external/external_media_tests/media_utils/video_puppeteer.py b/dom/media/test/external/external_media_tests/media_utils/video_puppeteer.py
new file mode 100644
index 000000000..b904267dd
--- /dev/null
+++ b/dom/media/test/external/external_media_tests/media_utils/video_puppeteer.py
@@ -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]
diff --git a/dom/media/test/external/external_media_tests/media_utils/youtube_puppeteer.py b/dom/media/test/external/external_media_tests/media_utils/youtube_puppeteer.py
new file mode 100644
index 000000000..e42bbcc87
--- /dev/null
+++ b/dom/media/test/external/external_media_tests/media_utils/youtube_puppeteer.py
@@ -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)
diff --git a/dom/media/test/external/external_media_tests/playback/eme.ini b/dom/media/test/external/external_media_tests/playback/eme.ini
new file mode 100644
index 000000000..6f08919bf
--- /dev/null
+++ b/dom/media/test/external/external_media_tests/playback/eme.ini
@@ -0,0 +1 @@
+[test_eme_playback.py]
diff --git a/dom/media/test/external/external_media_tests/playback/limiting_bandwidth.ini b/dom/media/test/external/external_media_tests/playback/limiting_bandwidth.ini
new file mode 100644
index 000000000..77c144d80
--- /dev/null
+++ b/dom/media/test/external/external_media_tests/playback/limiting_bandwidth.ini
@@ -0,0 +1,2 @@
+[test_playback_limiting_bandwidth.py]
+[test_ultra_low_bandwidth.py]
diff --git a/dom/media/test/external/external_media_tests/playback/manifest.ini b/dom/media/test/external/external_media_tests/playback/manifest.ini
new file mode 100644
index 000000000..f7271cbfb
--- /dev/null
+++ b/dom/media/test/external/external_media_tests/playback/manifest.ini
@@ -0,0 +1 @@
+[test_video_playback.py]
diff --git a/dom/media/test/external/external_media_tests/playback/netflix_limiting_bandwidth.ini b/dom/media/test/external/external_media_tests/playback/netflix_limiting_bandwidth.ini
new file mode 100644
index 000000000..dd0ce3601
--- /dev/null
+++ b/dom/media/test/external/external_media_tests/playback/netflix_limiting_bandwidth.ini
@@ -0,0 +1 @@
+[test_eme_playback_limiting_bandwidth.py]
diff --git a/dom/media/test/external/external_media_tests/playback/test_eme_playback.py b/dom/media/test/external/external_media_tests/playback/test_eme_playback.py
new file mode 100644
index 000000000..9c7eb6725
--- /dev/null
+++ b/dom/media/test/external/external_media_tests/playback/test_eme_playback.py
@@ -0,0 +1,18 @@
+# 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 external_media_harness.testcase import (
+ MediaTestCase,
+ VideoPlaybackTestsMixin,
+ EMESetupMixin
+)
+
+
+class TestEMEPlayback(MediaTestCase, VideoPlaybackTestsMixin, EMESetupMixin):
+
+ def setUp(self):
+ super(TestEMEPlayback, self).setUp()
+ self.check_eme_system()
+
+ # Tests are implemented in VideoPlaybackTestsMixin
diff --git a/dom/media/test/external/external_media_tests/playback/test_eme_playback_limiting_bandwidth.py b/dom/media/test/external/external_media_tests/playback/test_eme_playback_limiting_bandwidth.py
new file mode 100644
index 000000000..44cbb44f1
--- /dev/null
+++ b/dom/media/test/external/external_media_tests/playback/test_eme_playback_limiting_bandwidth.py
@@ -0,0 +1,24 @@
+# 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 marionette_harness import BrowserMobProxyTestCaseMixin
+
+from external_media_harness.testcase import (
+ EMESetupMixin,
+ NetworkBandwidthTestCase,
+ NetworkBandwidthTestsMixin,
+)
+
+
+class TestEMEPlaybackLimitingBandwidth(NetworkBandwidthTestCase,
+ BrowserMobProxyTestCaseMixin,
+ NetworkBandwidthTestsMixin,
+ EMESetupMixin):
+
+
+ def setUp(self):
+ super(TestEMEPlaybackLimitingBandwidth, self).setUp()
+ self.check_eme_system()
+
+ # Tests in NetworkBandwidthTestsMixin
diff --git a/dom/media/test/external/external_media_tests/playback/test_full_playback.py b/dom/media/test/external/external_media_tests/playback/test_full_playback.py
new file mode 100644
index 000000000..0db504682
--- /dev/null
+++ b/dom/media/test/external/external_media_tests/playback/test_full_playback.py
@@ -0,0 +1,25 @@
+# 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 marionette_harness import Marionette
+
+from external_media_harness.testcase import MediaTestCase
+from external_media_tests.media_utils.video_puppeteer import VideoPuppeteer
+
+
+class TestFullPlayback(MediaTestCase):
+ """ Test MSE playback in HTML5 video element.
+
+ These tests should pass on any site where a single video element plays
+ upon loading and is uninterrupted (by ads, for example). This will play
+ the full videos, so it could take a while depending on the videos playing.
+ It should be run much less frequently in automated systems.
+ """
+
+ def test_video_playback_full(self):
+ with self.marionette.using_context(Marionette.CONTEXT_CONTENT):
+ for url in self.video_urls:
+ video = VideoPuppeteer(self.marionette, url,
+ stall_wait_time=10)
+ self.run_playback(video)
diff --git a/dom/media/test/external/external_media_tests/playback/test_playback_limiting_bandwidth.py b/dom/media/test/external/external_media_tests/playback/test_playback_limiting_bandwidth.py
new file mode 100644
index 000000000..81f1e8a59
--- /dev/null
+++ b/dom/media/test/external/external_media_tests/playback/test_playback_limiting_bandwidth.py
@@ -0,0 +1,17 @@
+# 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 marionette_harness import BrowserMobProxyTestCaseMixin
+
+from external_media_harness.testcase import (
+ NetworkBandwidthTestCase, NetworkBandwidthTestsMixin
+)
+
+
+class TestPlaybackLimitingBandwidth(NetworkBandwidthTestCase,
+ NetworkBandwidthTestsMixin,
+ BrowserMobProxyTestCaseMixin):
+
+ # Tests are in NetworkBandwidthTestsMixin
+ pass
diff --git a/dom/media/test/external/external_media_tests/playback/test_shaka_playback.py b/dom/media/test/external/external_media_tests/playback/test_shaka_playback.py
new file mode 100644
index 000000000..a2ecb4a2c
--- /dev/null
+++ b/dom/media/test/external/external_media_tests/playback/test_shaka_playback.py
@@ -0,0 +1,42 @@
+# 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 marionette_harness import Marionette
+
+from external_media_harness.testcase import MediaTestCase
+from external_media_tests.media_utils.video_puppeteer import VideoPuppeteer
+
+
+class TestShakaPlayback(MediaTestCase):
+ """ Test Widevine playback in shaka-player
+
+ This test takes manifest URLs rather than URLs for pages with videos. These
+ manifests are loaded with shaka-player
+ """
+
+ def test_video_playback_partial(self):
+ """ Plays 60 seconds of the video from the manifest URLs given
+ """
+ shakaUrl = "http://shaka-player-demo.appspot.com"
+ self.marionette.set_pref('media.mediasource.webm.enabled', True)
+
+ with self.marionette.using_context(Marionette.CONTEXT_CONTENT):
+ for manifestUrl in self.video_urls:
+ vp = VideoPuppeteer(self.marionette,
+ shakaUrl,
+ stall_wait_time=10,
+ set_duration=60,
+ video_selector="video#video",
+ autostart=False)
+
+
+ manifestInput = self.marionette.find_element("id",
+ "manifestUrlInput")
+ manifestInput.clear()
+ manifestInput.send_keys(manifestUrl)
+ loadButton = self.marionette.find_element("id", "loadButton")
+ loadButton.click()
+
+ vp.start()
+ self.run_playback(vp)
diff --git a/dom/media/test/external/external_media_tests/playback/test_ultra_low_bandwidth.py b/dom/media/test/external/external_media_tests/playback/test_ultra_low_bandwidth.py
new file mode 100644
index 000000000..d49ff7d94
--- /dev/null
+++ b/dom/media/test/external/external_media_tests/playback/test_ultra_low_bandwidth.py
@@ -0,0 +1,15 @@
+# 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 marionette_harness import BrowserMobProxyTestCaseMixin
+
+from external_media_harness.testcase import NetworkBandwidthTestCase
+
+
+class TestUltraLowBandwidth(NetworkBandwidthTestCase,
+ BrowserMobProxyTestCaseMixin):
+
+ def test_playback_limiting_bandwidth_160(self):
+ self.proxy.limits({'downstream_kbps': 160})
+ self.run_videos(timeout=120)
diff --git a/dom/media/test/external/external_media_tests/playback/test_video_playback.py b/dom/media/test/external/external_media_tests/playback/test_video_playback.py
new file mode 100644
index 000000000..b841ec08c
--- /dev/null
+++ b/dom/media/test/external/external_media_tests/playback/test_video_playback.py
@@ -0,0 +1,15 @@
+# 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 external_media_harness.testcase import (
+ MediaTestCase,
+ VideoPlaybackTestsMixin
+)
+
+
+class TestVideoPlayback(MediaTestCase, VideoPlaybackTestsMixin):
+
+ # Tests are actually implemented in VideoPlaybackTestsMixin.
+
+ pass
diff --git a/dom/media/test/external/external_media_tests/playback/youtube/manifest.ini b/dom/media/test/external/external_media_tests/playback/youtube/manifest.ini
new file mode 100644
index 000000000..d9ad7eb19
--- /dev/null
+++ b/dom/media/test/external/external_media_tests/playback/youtube/manifest.ini
@@ -0,0 +1 @@
+[test_basic_playback.py ]
diff --git a/dom/media/test/external/external_media_tests/playback/youtube/test_basic_playback.py b/dom/media/test/external/external_media_tests/playback/youtube/test_basic_playback.py
new file mode 100644
index 000000000..edd0afc5e
--- /dev/null
+++ b/dom/media/test/external/external_media_tests/playback/youtube/test_basic_playback.py
@@ -0,0 +1,74 @@
+# 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 marionette_driver import Wait
+from marionette_driver.errors import TimeoutException
+from marionette_harness import Marionette
+
+from external_media_tests.utils import verbose_until
+from external_media_harness.testcase import MediaTestCase
+from external_media_tests.media_utils.video_puppeteer import VideoException
+from external_media_tests.media_utils.youtube_puppeteer import YouTubePuppeteer
+
+
+class TestBasicYouTubePlayback(MediaTestCase):
+ def test_mse_is_enabled_by_default(self):
+ with self.marionette.using_context(Marionette.CONTEXT_CONTENT):
+ youtube = YouTubePuppeteer(self.marionette, self.video_urls[0],
+ timeout=60)
+ wait = Wait(youtube,
+ timeout=min(300, youtube.expected_duration * 1.3),
+ interval=1)
+ try:
+ verbose_until(wait, youtube,
+ YouTubePuppeteer.mse_enabled,
+ "Failed to find 'blob' in video src url.")
+ except TimeoutException as e:
+ raise self.failureException(e)
+
+ def test_video_playing_in_one_tab(self):
+ with self.marionette.using_context(Marionette.CONTEXT_CONTENT):
+ for url in self.video_urls:
+ self.logger.info(url)
+ youtube = YouTubePuppeteer(self.marionette, url)
+ self.logger.info('Expected duration: {}'
+ .format(youtube.expected_duration))
+
+ final_piece = 60
+ try:
+ time_left = youtube.wait_for_almost_done(
+ final_piece=final_piece)
+ except VideoException as e:
+ raise self.failureException(e)
+ duration = abs(youtube.expected_duration) + 1
+ if duration > 1:
+ self.logger.info('Almost done: {} - {} seconds left.'
+ .format(url, time_left))
+ if time_left > final_piece:
+ self.marionette.log('time_left greater than '
+ 'final_piece - {}'
+ .format(time_left),
+ level='WARNING')
+ self.save_screenshot()
+ else:
+ self.marionette.log('Duration close to 0 - {}'
+ .format(youtube),
+ level='WARNING')
+ self.save_screenshot()
+ try:
+ verbose_until(Wait(youtube,
+ timeout=max(100, time_left) * 1.3,
+ interval=1),
+ youtube,
+ YouTubePuppeteer.playback_done)
+ except TimeoutException as e:
+ raise self.failureException(e)
+
+ def test_playback_starts(self):
+ with self.marionette.using_context(Marionette.CONTEXT_CONTENT):
+ for url in self.video_urls:
+ try:
+ YouTubePuppeteer(self.marionette, url, timeout=60)
+ except TimeoutException as e:
+ raise self.failureException(e)
diff --git a/dom/media/test/external/external_media_tests/playback/youtube/test_prefs.py b/dom/media/test/external/external_media_tests/playback/youtube/test_prefs.py
new file mode 100644
index 000000000..4c07ca008
--- /dev/null
+++ b/dom/media/test/external/external_media_tests/playback/youtube/test_prefs.py
@@ -0,0 +1,46 @@
+# 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 external_media_harness.testcase import MediaTestCase
+from marionette_driver import Wait
+
+from external_media_tests.utils import verbose_until
+from external_media_tests.media_utils.youtube_puppeteer import YouTubePuppeteer
+
+
+class TestMediaSourcePrefs(MediaTestCase):
+ def setUp(self):
+ MediaTestCase.setUp(self)
+ self.test_urls = self.video_urls[:2]
+ self.max_timeout = 60
+
+ def tearDown(self):
+ MediaTestCase.tearDown(self)
+
+ def test_mse_prefs(self):
+ """ mediasource should only be used if MSE prefs are enabled."""
+ self.set_mse_enabled_prefs(False)
+ self.check_mse_src(False, self.test_urls[0])
+
+ self.set_mse_enabled_prefs(True)
+ self.check_mse_src(True, self.test_urls[0])
+
+ def set_mse_enabled_prefs(self, value):
+ with self.marionette.using_context('chrome'):
+ self.marionette.set_pref('media.mediasource.enabled', value)
+ self.marionette.set_pref('media.mediasource.mp4.enabled', value)
+ self.marionette.set_pref('media.mediasource.webm.enabled', value)
+
+ def check_mse_src(self, mse_expected, url):
+ with self.marionette.using_context('content'):
+ youtube = YouTubePuppeteer(self.marionette, url)
+ wait = Wait(youtube,
+ timeout=min(self.max_timeout,
+ youtube.expected_duration * 1.3),
+ interval=1)
+
+ def cond(y):
+ return y.mse_enabled == mse_expected
+
+ verbose_until(wait, youtube, cond)
diff --git a/dom/media/test/external/external_media_tests/resources/mozilla.html b/dom/media/test/external/external_media_tests/resources/mozilla.html
new file mode 100644
index 000000000..1cdf0fb4f
--- /dev/null
+++ b/dom/media/test/external/external_media_tests/resources/mozilla.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<html lang="en" dir="ltr">
+<head>
+ <title>Mozilla</title>
+ <link rel="shortcut icon" type="image/ico" href="../images/mozilla_favicon.ico" />
+</head>
+
+<body>
+ <a href="mozilla.html">
+ <img id="mozilla_logo" src="../images/mozilla_logo.jpg" />
+ </a>
+
+ <a href="#community">RARARARARARA</a> |
+ <a href="#project">Project</a> |
+ <a href="#organization">Organization</a>
+
+ <div id="content">
+ <h1 id="page-title">
+ <strong>RARARARARARA</strong> that the internet should be public,
+ open and accessible.
+ </h1>
+
+ <h2><a name="community">RARARARARARA</a></h2>
+ <p id="community">
+ We're a global community of thousands who believe in the power
+ of technology to enrich people's lives.
+ <a href="mozilla_community.html">More</a>
+ </p>
+
+ <h2><a name="project">Project</a></h2>
+ <p id="project">
+ We're an open source project whose code is used for some of the
+ Internet's most innovative applications.
+ <a href="mozilla_projects.html">More</a>
+ </p>
+
+ <h2><a name="organization">Organization</a></h2>
+ <p id="organization">
+ We're a public benefit organization dedicated to making the
+ Internet better for everyone.
+ <a href="mozilla_mission.html">More</a>
+ </p>
+ </div>
+</body>
+</html>
diff --git a/dom/media/test/external/external_media_tests/test_example.py b/dom/media/test/external/external_media_tests/test_example.py
new file mode 100644
index 000000000..99782014f
--- /dev/null
+++ b/dom/media/test/external/external_media_tests/test_example.py
@@ -0,0 +1,21 @@
+from marionette_harness import Marionette
+
+from external_media_harness.testcase import MediaTestCase
+
+
+class TestSomethingElse(MediaTestCase):
+ def setUp(self):
+ MediaTestCase.setUp(self)
+ self.test_urls = [
+ 'mozilla.html',
+ ]
+ self.test_urls = [self.marionette.absolute_url(t)
+ for t in self.test_urls]
+
+ def tearDown(self):
+ MediaTestCase.tearDown(self)
+
+ def test_foo(self):
+ self.logger.info('foo!')
+ with self.marionette.using_context(Marionette.CONTEXT_CONTENT):
+ self.marionette.navigate(self.test_urls[0])
diff --git a/dom/media/test/external/external_media_tests/urls/default.ini b/dom/media/test/external/external_media_tests/urls/default.ini
new file mode 100644
index 000000000..b1e26abbd
--- /dev/null
+++ b/dom/media/test/external/external_media_tests/urls/default.ini
@@ -0,0 +1,9 @@
+# short videos; no ads; embedded; max 5 minutes
+# 0:12
+[https://youtube.com/embed/AbAACm1IQE0?autoplay=1]
+# 2:18
+[https://youtube.com/embed/yOQQCoxs8-k?autoplay=1]
+# 0:08
+[https://youtube.com/embed/1visYpIREUM?autoplay=1]
+# 2:09
+[https://youtube.com/embed/rjmuKV9BTkE?autoplay=1]
diff --git a/dom/media/test/external/external_media_tests/urls/netflix/default.ini b/dom/media/test/external/external_media_tests/urls/netflix/default.ini
new file mode 100644
index 000000000..ed14b69b8
--- /dev/null
+++ b/dom/media/test/external/external_media_tests/urls/netflix/default.ini
@@ -0,0 +1,8 @@
+# YouTube test
+#[https://www.youtube.com/watch?v=AbAACm1IQE0]
+# ClearKey - 11:07
+[http://www.netflix.com/watch/70136810]
+# NoDRM - 2:24:xx
+[http://www.netflix.com/watch/70304192]
+# DRM - 24:47
+[http://www.netflix.com/watch/80015538]
diff --git a/dom/media/test/external/external_media_tests/urls/shaka-player/default.ini b/dom/media/test/external/external_media_tests/urls/shaka-player/default.ini
new file mode 100644
index 000000000..8a42d7603
--- /dev/null
+++ b/dom/media/test/external/external_media_tests/urls/shaka-player/default.ini
@@ -0,0 +1,30 @@
+# The Shaka-player tests take manifest URLs rather than URLs for a page that
+# plays a video (since shaka-player is the page that plays the video)
+# This file contains the manifest URLs that shaka-player provides in it's
+# dropdown
+
+# "Angel One" (TNG clip) - multilingual, subtitles, VP8
+[http://shaka-player-demo.appspot.com/assets/angel_one.mpd]
+# "Car" (YT DASH test) - MP4
+[http://shaka-player-demo.appspot.com/assets/car-20120827-manifest.mpd]
+# "Car/CENC" (YT DASH EME test) - MP4, ClearKey
+[http://shaka-player-demo.appspot.com/assets/car_cenc-20120827-manifest.mpd]
+# "Feelings" (YT DASH test) - VP9
+[http://shaka-player-demo.appspot.com/assets/feelings_vp9-20130806-manifest.mpd]
+# "Feelings" (YT DASH test) - Audio only
+[http://shaka-player-demo.appspot.com/assets/feelings_audio_only-20130806-manifest.mpd]
+# "Car/SegmentTemplate" (Chromecast test) - MP4 (no SIDX, video only), Widevine
+[http://shaka-player-demo.appspot.com/assets/car_segmenttemplate.mpd]
+# "GPAC/SegmentList" (conformance test)
+[http://download.tsi.telecom-paristech.fr/gpac/DASH_CONFORMANCE/TelecomParisTech/mp4-main-multi/mp4-main-multi-mpd-AV-NBS.mpd]
+# "Oops" (modified YT DASH EME test) - MP4, multi-DRM
+[http://shaka-player-demo.appspot.com/assets/oops_cenc-20121114-signedlicenseurl-manifest.mpd]
+# "Oops" (modified YT DASH EME test) - MP4, Widevine, PSSH in MPD
+# This stream currently does not load
+#[http://shaka-player-demo.appspot.com/assets/oops_cenc_pssh.mpd]
+# "Sintel" (1080p high bitrate test) - MP4
+[http://storage.googleapis.com/widevine-demo-media/sintel-1080p/dash.mpd]
+# "Sintel" (4k) - MP4, VP8, VP9
+[http://storage.googleapis.com/widevine-demo-media/sintel-multicodec-4k/dash.mpd]
+# "Sintel" (4k) - MP4, Widevine
+[http://storage.googleapis.com/widevine-demo-media/sintel-4k-widevine/sintel.mpd]
diff --git a/dom/media/test/external/external_media_tests/urls/youtube/archive/crash_videos.ini b/dom/media/test/external/external_media_tests/urls/youtube/archive/crash_videos.ini
new file mode 100644
index 000000000..e7d420254
--- /dev/null
+++ b/dom/media/test/external/external_media_tests/urls/youtube/archive/crash_videos.ini
@@ -0,0 +1,25 @@
+[https://www.youtube.com/watch?v=2GfaRuIMdos]
+[https://www.youtube.com/watch?v=9vKvcCNt40g]
+[https://www.youtube.com/watch?v=SHLLHya2pNo]
+[https://www.youtube.com/watch?v=isMEMDE2enU]
+[https://www.youtube.com/watch?v=H81M_MebLsk]
+[https://www.youtube.com/watch?v=yopNkcDzQQw]
+[https://www.youtube.com/watch?v=r_bG5beSqw0]
+[https://www.youtube.com/watch?v=Ki9sSZKClO0]
+[https://www.youtube.com/watch?v=gNS04P8djk4]
+[https://www.youtube.com/watch?v=DwC_6fIBW0w]
+[https://www.youtube.com/watch?v=g1D3A14o0NA]
+[https://www.youtube.com/watch?v=cs-XZ_dN4Hc]
+[https://www.youtube.com/watch?v=ZEWZ3AAH98c]
+[https://www.youtube.com/watch?v=hwbVGE4GBJI]
+[https://www.youtube.com/watch?v=cvcMnbkasIs]
+[https://www.youtube.com/watch?v=cHaBuoHwQ0Y]
+[https://www.youtube.com/watch?v=VKIYoAG9MZ0]
+[https://www.youtube.com/watch?v=WWDb2_unEJc]
+[https://www.youtube.com/watch?v=ybw5zonQffE]
+[https://www.youtube.com/watch?v=hS6ps2Xph_o]
+[https://www.youtube.com/watch?v=Bjb3xhgIqv4]
+[https://www.youtube.com/watch?v=fOzvEhX4Kvk]
+[https://www.youtube.com/watch?v=_TNsUxp_BxM]
+[https://www.youtube.com/watch?v=QRdwCSHF3oo]
+[https://www.youtube.com/watch?v=VwaHFcKJSYA]
diff --git a/dom/media/test/external/external_media_tests/urls/youtube/archive/other_videos.ini b/dom/media/test/external/external_media_tests/urls/youtube/archive/other_videos.ini
new file mode 100644
index 000000000..732d2405c
--- /dev/null
+++ b/dom/media/test/external/external_media_tests/urls/youtube/archive/other_videos.ini
@@ -0,0 +1,19 @@
+# backlog of videos
+[http://youtu.be/2iVAvSnofy8]
+
+# 300s <= duration <= 1200s (5-20min)
+[http://youtu.be/9bZkp7q19f0]
+[http://youtu.be/KQ6zr6kCPj8]
+
+# duration > 1200s (>20min)
+[http://youtu.be/wZZ7oFKsKzY]
+[http://youtu.be/eHUrC_UiZwY]
+[http://youtu.be/FLX64H5FYa8]
+[http://youtu.be/Fu2DcHzokew]
+
+#no_ad_tests_youtube
+#[http://youtu.be/pWI8RB2dmfU]
+#[http://youtu.be/6GBtEmtVObw]
+
+#playlist_tests_youtube
+#[http://youtu.be/R6KJjPqlPz4?list=PL75_HhpYGJQ1Fzv9a46FlHfiy-fJusKBZ]
diff --git a/dom/media/test/external/external_media_tests/urls/youtube/archive/video_data.ini b/dom/media/test/external/external_media_tests/urls/youtube/archive/video_data.ini
new file mode 100644
index 000000000..ff8f58866
--- /dev/null
+++ b/dom/media/test/external/external_media_tests/urls/youtube/archive/video_data.ini
@@ -0,0 +1,21 @@
+# duration < 300s (5min)
+[http://youtu.be/065dlrJoHcw]
+[http://youtu.be/1visYpIREUM]
+[http://youtu.be/mDf7CR5QKcE]
+[http://youtu.be/Aebs62bX0dA]
+[http://youtu.be/6SFp1z7uA6g]
+[http://youtu.be/tDDVAErOI5U]
+
+# ad testing
+[https://www.youtube.com/watch?v=l5ODwR6FPRQ]
+[https://www.youtube.com/watch?v=7RMQksXpQSk]
+
+# duration > 5 min
+# video with ad in the middle
+[https://www.youtube.com/watch?v=cht9Xq9suGg]
+
+# long video (>30 min), no ads
+[https://www.youtube.com/watch?v=-qXxNPvqHtQ]
+
+# bug 1144172, duration ~ 1hr
+#[https://www.youtube.com/watch?v=AYYDshv8C4g]
diff --git a/dom/media/test/external/external_media_tests/urls/youtube/archive/youtube.ini b/dom/media/test/external/external_media_tests/urls/youtube/archive/youtube.ini
new file mode 100644
index 000000000..0676b9ef4
--- /dev/null
+++ b/dom/media/test/external/external_media_tests/urls/youtube/archive/youtube.ini
@@ -0,0 +1,38 @@
+# < 1 no ads
+[https://youtu.be/AbAACm1IQE0]
+[https://www.youtube.com/watch?v=KdHZwWQWNyM]
+[https://www.youtube.com/watch?v=-hVmkA_I9EE]
+[https://www.youtube.com/watch?v=1visYpIREUM]
+
+# 1 < t <= 5 no ads
+[https://www.youtube.com/watch?v=rpYRAs6ePY8]
+[https://www.youtube.com/watch?v=xcgUKzwg0Mo]
+[https://youtu.be/sEAT2EFIJow]
+[https://www.youtube.com/watch?v=SSgnbQ5UC48]
+[https://youtu.be/4oQu26IhiaA]
+[https://youtu.be/IbND63HOb0M]
+[https://youtu.be/-9sJp9wrdAk]
+[https://www.youtube.com/watch?v=yIQGH4aQWI0]
+
+# 1 < t <= 5
+[https://www.youtube.com/watch?v=-hVmkA_I9EE]
+[https://www.youtube.com/watch?v=l5ODwR6FPRQ]
+[https://www.youtube.com/watch?v=7RMQksXpQSk]
+[https://www.youtube.com/watch?v=TsXMe8H6iyc]
+[https://www.youtube.com/watch?v=tDDVAErOI5U]
+
+# 5 < t <= 10
+[https://youtu.be/Tl-hI2IsCo0] # no ad
+[https://www.youtube.com/watch?v=IX_d_vMKswE] #no ad
+[https://www.youtube.com/watch?v=YVQeTY-Ayko] #no ad
+[https://www.youtube.com/watch?v=rE3j_RHkqJc]
+[https://www.youtube.com/watch?v=l4bmZ1gRqCc]
+
+# 10 < t <= 30
+[https://www.youtube.com/watch?v=RvymAHt3nPc] # no ads
+[https://www.youtube.com/watch?v=8XQ1onjXJK0]
+[https://www.youtube.com/watch?v=6Lm9EHhbJAY]
+[https://www.youtube.com/watch?v=cht9Xq9suGg]
+
+# long video (>30 min), no ads
+[https://www.youtube.com/watch?v=-qXxNPvqHtQ]
diff --git a/dom/media/test/external/external_media_tests/urls/youtube/long1-720.ini b/dom/media/test/external/external_media_tests/urls/youtube/long1-720.ini
new file mode 100644
index 000000000..cabb823b1
--- /dev/null
+++ b/dom/media/test/external/external_media_tests/urls/youtube/long1-720.ini
@@ -0,0 +1,5 @@
+# a couple of very long videos, < 12 hours total
+# 6:00:00 - can't embed due to copyright
+[https://www.youtube.com/watch?v=5N8sUccRiTA]
+# 2:09:00
+[https://www.youtube.com/embed/b6q5N16dje4?autoplay=1]
diff --git a/dom/media/test/external/external_media_tests/urls/youtube/long2-crashes-720.ini b/dom/media/test/external/external_media_tests/urls/youtube/long2-crashes-720.ini
new file mode 100644
index 000000000..de449f882
--- /dev/null
+++ b/dom/media/test/external/external_media_tests/urls/youtube/long2-crashes-720.ini
@@ -0,0 +1,39 @@
+# It appears these are not currently used by tests. They are left here as they
+# reference failure scenarios. If tese are fixed that can be removed.
+
+# videos from crashes, < 12 hours
+
+# hang | NtUserMessageCall | SendMessageW
+# 1:10:00
+[https://www.youtube.com/watch?v=Ztie4DqeOak]
+
+# nsPluginInstanceOwner::GetDocument(nsIDocument**)
+# 22:40
+[https://www.youtube.com/watch?v=D4cLM_JRrAU]
+# 16:47
+[https://www.youtube.com/watch?v=3C2r05Lxsrk]
+
+# F1398665248_____________________________
+# 1:06:00
+[https://www.youtube.com/watch?v=59gTMBss8o0]
+# 50:58
+[https://www.youtube.com/watch?v=_7VFIZhR744]
+# 44:54
+[https://www.youtube.com/watch?v=d6ro4Oq5msA]
+
+# hang | WaitForMultipleObjectsEx | RealMsgWaitForMultipleObjectsEx | MsgWaitForMultipleObjects | F_1152915508___________________________________
+#1:07:12
+[https://www.youtube.com/watch?v=Ffkf3tosmKw]
+# 1:02:00
+[https://www.youtube.com/watch?v=dC3AHEao2MI]
+
+# hang | BaseGetNamedObjectDirectory | RealMsgWaitForMultipleObjectsEx | MsgWaitForMultipleObjects | F_1152915508___________________________________
+# 10:00
+[https://www.youtube.com/watch?v=fn3Qb56ujNQ]
+# 5:00
+[https://www.youtube.com/watch?v=gBsh1bT8ltI]
+# 03:50:12
+[https://www.youtube.com/watch?v=TdW4S8zbmJQ]
+
+
+
diff --git a/dom/media/test/external/external_media_tests/urls/youtube/long3-crashes-900.ini b/dom/media/test/external/external_media_tests/urls/youtube/long3-crashes-900.ini
new file mode 100644
index 000000000..70081b986
--- /dev/null
+++ b/dom/media/test/external/external_media_tests/urls/youtube/long3-crashes-900.ini
@@ -0,0 +1,86 @@
+# It appears these are not currently used by tests. They are left here as they
+# reference failure scenarios. If tese are fixed that can be removed.
+
+# Total time: about 12-13 hours + unskippable ads
+#Request url: https://crash-stats.mozilla.com/api/SuperSearchUnredacted/?product=Firefox&url=%24https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D&url=%21~list&url=%21~index&_results_number=50&platform=Windows&version=37.0&date=%3E2015-03-26
+
+#Request url: https://crash-stats.mozilla.com/api/SuperSearchUnredacted/?product=Firefox&url=%24https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D&url=%21~list&url=%21~index&_results_number=5&platform=Windows&version=37.0&signature=%3Dhang+%7C+NtUserMessageCall+%7C+SendMessageW&date=%3E2015-03-26
+
+#Request url: https://crash-stats.mozilla.com/api/SuperSearchUnredacted/?product=Firefox&url=%24https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D&url=%21~list&url=%21~index&_results_number=5&platform=Windows&version=37.0&signature=%3DOOM+%7C+small&date=%3E2015-03-26
+
+#Request url: https://crash-stats.mozilla.com/api/SuperSearchUnredacted/?product=Firefox&url=%24https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D&url=%21~list&url=%21~index&_results_number=5&platform=Windows&version=37.0&signature=%3Dmozilla%3A%3Alayers%3A%3ACompositorD3D11%3A%3AHandleError%28long%2C+mozilla%3A%3Alayers%3A%3ACompositorD3D11%3A%3ASeverity%29+%7C+mozilla%3A%3Alayers%3A%3ACompositorD3D11%3A%3AFailed%28long%2C+mozilla%3A%3Alayers%3A%3ACompositorD3D11%3A%3ASeverity%29+%7C+mozilla%3A%3Alayers%3A%3ACompositorD3D11%3A%3AUpdateRenderTarget%28%29&date=%3E2015-03-26
+
+#Request url: https://crash-stats.mozilla.com/api/SuperSearchUnredacted/?product=Firefox&url=%24https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D&url=%21~list&url=%21~index&_results_number=5&platform=Windows&version=37.0&signature=%3DOOM+%7C+large+%7C+mozalloc_abort%28char+const%2A+const%29+%7C+mozalloc_handle_oom%28unsigned+int%29+%7C+moz_xmalloc+%7C+nsTArray_base%3CnsTArrayInfallibleAllocator%2C+nsTArray_CopyWithMemutils%3E%3A%3AEnsureCapacity%28unsigned+int%2C+unsigned+int%29+%7C+nsTArray_base%3CnsTArrayInfallibleAllo...&date=%3E2015-03-26
+
+#Request url: https://crash-stats.mozilla.com/api/SuperSearchUnredacted/?product=Firefox&url=%24https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D&url=%21~list&url=%21~index&_results_number=5&platform=Windows&version=37.0&signature=%3Dshutdownhang+%7C+WaitForSingleObjectEx+%7C+WaitForSingleObject+%7C+PR_Wait+%7C+nsThread%3A%3AProcessNextEvent%28bool%2C+bool%2A%29+%7C+NS_ProcessNextEvent%28nsIThread%2A%2C+bool%29+%7C+mozilla%3A%3AMediaShutdownManager%3A%3AShutdown%28%29&date=%3E2015-03-26
+
+#Request url: https://crash-stats.mozilla.com/api/SuperSearchUnredacted/?product=Firefox&url=%24https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D&url=%21~list&url=%21~index&_results_number=5&platform=Windows&version=37.0&signature=%3Dmozilla%3A%3Alayers%3A%3ACompositorD3D11%3A%3AUpdateConstantBuffers%28%29&date=%3E2015-03-26
+
+#Request url: https://crash-stats.mozilla.com/api/SuperSearchUnredacted/?product=Firefox&url=%24https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D&url=%21~list&url=%21~index&_results_number=5&platform=Windows&version=37.0&signature=%3Dmsvcr120.dll%400xf20c&date=%3E2015-03-26
+
+#Request url: https://crash-stats.mozilla.com/api/SuperSearchUnredacted/?product=Firefox&url=%24https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D&url=%21~list&url=%21~index&_results_number=5&platform=Windows&version=37.0&signature=%3Djs%3A%3AGCMarker%3A%3AprocessMarkStackTop%28js%3A%3ASliceBudget%26%29&date=%3E2015-03-26
+
+#Request url: https://crash-stats.mozilla.com/api/SuperSearchUnredacted/?product=Firefox&url=%24https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D&url=%21~list&url=%21~index&_results_number=5&platform=Windows&version=37.0&signature=%3Dshutdownhang+%7C+WaitForSingleObjectEx+%7C+WaitForSingleObject+%7C+PR_Wait+%7C+nsThread%3A%3AProcessNextEvent%28bool%2C+bool%2A%29+%7C+NS_ProcessNextEvent%28nsIThread%2A%2C+bool%29+%7C+mozilla%3A%3Alayers%3A%3ACompositorParent%3A%3AShutDown%28%29&date=%3E2015-03-26
+
+#Request url: https://crash-stats.mozilla.com/api/SuperSearchUnredacted/?product=Firefox&url=%24https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D&url=%21~list&url=%21~index&_results_number=5&platform=Windows&version=37.0&signature=%3Dshutdownhang+%7C+WaitForSingleObjectEx+%7C+WaitForSingleObject+%7C+PR_Wait+%7C+mozilla%3A%3AReentrantMonitor%3A%3AWait%28unsigned+int%29+%7C+mozilla%3A%3Alayers%3A%3AImageBridgeChild%3A%3AShutDown%28%29&date=%3E2015-03-26
+
+# shutdownhang | WaitForSingleObjectEx | WaitForSingleObject | PR_Wait | nsThread::ProcessNextEvent(bool, bool*) | NS_ProcessNextEvent(nsIThread*, bool) | mozilla::MediaShutdownManager::Shutdown()
+[https://www.youtube.com/watch?v=PnwS01Yu9bs]
+[https://www.youtube.com/watch?v=6hNOMhEqI9g]
+[https://www.youtube.com/watch?v=gK9eCjYEwH4]
+#[https://www.youtube.com/watch?v=E9DFupLEV7c] Geographic restriction
+[https://www.youtube.com/watch?v=sLEVm0OGImU]
+# hang | NtUserMessageCall | SendMessageW
+[https://www.youtube.com/watch?v=kt0g4dWxEBo]
+[https://www.youtube.com/watch?v=cvwMS6UmesQ]
+[https://www.youtube.com/watch?v=Bj3YSTu3jUs]
+[https://www.youtube.com/watch?v=J9bgaoXLbFI]
+[https://www.youtube.com/watch?v=d5GUd6IElIw]
+# shutdownhang | WaitForSingleObjectEx | WaitForSingleObject | PR_Wait | mozilla::ReentrantMonitor::Wait(unsigned int) | mozilla::layers::ImageBridgeChild::ShutDown()
+[https://www.youtube.com/watch?v=6FMNFvKEy4c]
+[https://www.youtube.com/watch?v=w4RNIyJw9RI]
+#[https://www.youtube.com/watch?v=tKB5S1yp5MA] Account terminated
+[https://www.youtube.com/watch?v=Tct2Iv1QRUU]
+[https://www.youtube.com/watch?v=zDHOW9PdQYE]
+# shutdownhang | WaitForSingleObjectEx | WaitForSingleObject | PR_Wait | nsThread::ProcessNextEvent(bool, bool*) | NS_ProcessNextEvent(nsIThread*, bool) | mozilla::layers::CompositorParent::ShutDown()
+[https://www.youtube.com/watch?v=AGo24nC3_HU]
+[https://www.youtube.com/watch?v=GsVaCnud57U]
+[https://www.youtube.com/watch?v=zFg55zva7ok]
+#[https://www.youtube.com/watch?v=5VSk7bwPPOM] Policy violation
+[https://www.youtube.com/watch?v=2OYa5kR5EQ4]
+# OOM | large | mozalloc_abort(char const* const) | mozalloc_handle_oom(unsigned int) | moz_xmalloc | nsTArray_base<nsTArrayInfallibleAllocator, nsTArray_CopyWithMemutils>::EnsureCapacity(unsigned int, unsigned int) | nsTArray_base<nsTArrayInfallibleAllo...
+#[https://www.youtube.com/watch?v=1g91CAubt1c] Policy violation
+[https://www.youtube.com/watch?v=HE_7UFHPfQ0]
+# [https://www.youtube.com/watch?v=vhrM1JXG8-k] Live stream, Flash only
+[https://www.youtube.com/watch?v=ERWFf0JS94E]
+#[https://www.youtube.com/watch?v=8tmiawwVreE] Age restriction
+# mozilla::layers::CompositorD3D11::UpdateConstantBuffers()
+[https://www.youtube.com/watch?v=7azYa518LvE]
+[https://www.youtube.com/watch?v=Zg5JvdXHUqg]
+[https://www.youtube.com/watch?v=Q_kcoEY2wNw]
+[https://www.youtube.com/watch?v=eNzUJa0WjfU]
+[https://www.youtube.com/watch?v=B5V12xYb7hE]
+# OOM | small
+[https://www.youtube.com/watch?v=TS9Z8dN4OPo]
+[https://www.youtube.com/watch?v=EpngdStzhmQ]
+[https://www.youtube.com/watch?v=dUiDCX3BnM0]
+[https://www.youtube.com/watch?v=Ii4Su6Z8pCw]
+[https://www.youtube.com/watch?v=vviBJS6WQno]
+# msvcr120.dll@0xf20c
+[https://www.youtube.com/watch?v=hRE2VO9oa_g]
+[https://www.youtube.com/watch?v=qLL8VanC3zI]
+[https://www.youtube.com/watch?v=YX2LIztg2EI]
+[https://www.youtube.com/watch?v=-7Eh28eatBo]
+[https://www.youtube.com/watch?v=a32AMX55sZM]
+# js::GCMarker::processMarkStackTop(js::SliceBudget&)
+[https://www.youtube.com/watch?v=f0L2RzygE5k]
+[https://www.youtube.com/watch?v=-1RGIDgwHgM]
+[https://www.youtube.com/watch?v=iL1CEn7SQfQ]
+[https://www.youtube.com/watch?v=450p7goxZqg]
+[https://www.youtube.com/watch?v=Eo8c2sZ2eOY]
+# mozilla::layers::CompositorD3D11::HandleError(long, mozilla::layers::CompositorD3D11::Severity) | mozilla::layers::CompositorD3D11::Failed(long, mozilla::layers::CompositorD3D11::Severity) | mozilla::layers::CompositorD3D11::UpdateRenderTarget()
+[https://www.youtube.com/watch?v=a79R7bPhVhw]
+[https://www.youtube.com/watch?v=JRNCgvZs5v4]
+[https://www.youtube.com/watch?v=q8y58dWKfY8]
+[https://www.youtube.com/watch?v=Ns9M6sUvqxs]
+[https://www.youtube.com/watch?v=Ii-PCeTgR-A]
diff --git a/dom/media/test/external/external_media_tests/urls/youtube/medium1-60.ini b/dom/media/test/external/external_media_tests/urls/youtube/medium1-60.ini
new file mode 100644
index 000000000..65ccef11a
--- /dev/null
+++ b/dom/media/test/external/external_media_tests/urls/youtube/medium1-60.ini
@@ -0,0 +1,18 @@
+# mix of shorter/longer videos with/without ads, < 60 min
+# 4:59 - can't embed
+[https://www.youtube.com/watch?v=pWI8RB2dmfU]
+# 0:46 ad at start
+[https://www.youtube.com/embed/6SFp1z7uA6g?autoplay=1]
+# 0:58 ad at start
+[https://www.youtube.com/embed/Aebs62bX0dA?autoplay=1]
+# 1:43 ad
+[https://www.youtube.com/embed/l5ODwR6FPRQ?autoplay=1]
+# 8:00 ad - can't embed
+[https://www.youtube.com/watch?v=KlyXNRrsk4A]
+# video with ad in beginning and in the middle 20:00
+# https://bugzilla.mozilla.org/show_bug.cgi?id=1176815
+[https://www.youtube.com/embed/cht9Xq9suGg?autoplay=1]
+# 1:35 ad
+[https://www.youtube.com/embed/orybDrUj4vA?autoplay=1]
+# 3:02 ad
+[https://www.youtube.com/embed/tDDVAErOI5U?autoplay=1]
diff --git a/dom/media/test/external/external_media_tests/urls/youtube/short1-10.ini b/dom/media/test/external/external_media_tests/urls/youtube/short1-10.ini
new file mode 100644
index 000000000..a8b4016dc
--- /dev/null
+++ b/dom/media/test/external/external_media_tests/urls/youtube/short1-10.ini
@@ -0,0 +1,13 @@
+# short videos; no ads; max 10 minutes
+# 0:12
+[https://youtu.be/AbAACm1IQE0]
+# 0:30
+[https://www.youtube.com/watch?v=KdHZwWQWNyM]
+# 0:08
+[https://www.youtube.com/watch?v=1visYpIREUM]
+# 3:27
+[https://www.youtube.com/watch?v=xcgUKzwg0Mo]
+# 1:21
+[https://youtu.be/sEAT2EFIJow]
+# 1:23
+[https://www.youtube.com/watch?v=SSgnbQ5UC48]
diff --git a/dom/media/test/external/external_media_tests/urls/youtube/short2-crashes-15.ini b/dom/media/test/external/external_media_tests/urls/youtube/short2-crashes-15.ini
new file mode 100644
index 000000000..bfcba4101
--- /dev/null
+++ b/dom/media/test/external/external_media_tests/urls/youtube/short2-crashes-15.ini
@@ -0,0 +1,17 @@
+# It appears these are not currently used by tests. They are left here as they
+# reference failure scenarios. If tese are fixed that can be removed.
+
+# crash-data videos, < 15 minutes total
+
+# hang | NtUserMessageCall | SendMessageW
+# 5:40
+[https://www.youtube.com/watch?v=UIobdRNLNek]
+
+# F1398665248_____________________________
+# 3:59
+[https://www.youtube.com/watch?v=XGotQYd-X6o]
+
+# hang | WaitForMultipleObjectsEx | RealMsgWaitForMultipleObjectsEx | MsgWaitForMultipleObjects | F_1152915508___________________________________
+# 4:07
+[https://www.youtube.com/watch?v=wQgppPHXJSs]
+
diff --git a/dom/media/test/external/external_media_tests/utils.py b/dom/media/test/external/external_media_tests/utils.py
new file mode 100644
index 000000000..4ac0d5f62
--- /dev/null
+++ b/dom/media/test/external/external_media_tests/utils.py
@@ -0,0 +1,68 @@
+# 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 datetime
+import time
+import types
+
+from marionette_driver.errors import TimeoutException
+
+
+def timestamp_now():
+ return int(time.mktime(datetime.datetime.now().timetuple()))
+
+
+def verbose_until(wait, target, condition, message=""):
+ """
+ Performs a `wait`.until(condition)` and adds information about the state of
+ `target` to any resulting `TimeoutException`.
+
+ :param wait: a `marionette.Wait` instance
+ :param target: the object you want verbose output about if a
+ `TimeoutException` is raised
+ This is usually the input value provided to the `condition` used by
+ `wait`. Ideally, `target` should implement `__str__`
+ :param condition: callable function used by `wait.until()`
+ :param message: optional message to log when exception occurs
+
+ :return: the result of `wait.until(condition)`
+ """
+ if isinstance(condition, types.FunctionType):
+ name = condition.__name__
+ else:
+ name = str(condition)
+ err_message = '\n'.join([message,
+ 'condition: ' + name,
+ str(target)])
+
+ return wait.until(condition, message=err_message)
+
+
+
+def save_memory_report(marionette):
+ """
+ Saves memory report (like about:memory) to current working directory.
+
+ :param marionette: Marionette instance to use for executing.
+ """
+ with marionette.using_context('chrome'):
+ marionette.execute_async_script("""
+ Components.utils.import("resource://gre/modules/Services.jsm");
+ let Cc = Components.classes;
+ let Ci = Components.interfaces;
+ let dumper = Cc["@mozilla.org/memory-info-dumper;1"].
+ getService(Ci.nsIMemoryInfoDumper);
+ // Examples of dirs: "CurProcD" usually 'browser' dir in
+ // current FF dir; "DfltDwnld" default download dir
+ let file = Services.dirsvc.get("CurProcD", Ci.nsIFile);
+ file.append("media-memory-report");
+ file.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0777);
+ file.append("media-memory-report.json.gz");
+ dumper.dumpMemoryReportsToNamedFile(file.path, null, null, false);
+ log('Saved memory report to ' + file.path);
+ // for dmd-enabled build
+ dumper.dumpMemoryInfoToTempDir("media", false, false);
+ marionetteScriptFinished(true);
+ return;
+ """, script_timeout=30000)