Bug 1235606 - Move firefox-media-tests into mozilla-central. r?ted, r?cpearce draft
authorSyd Polk <spolk@mozilla.com>
Fri, 08 Jan 2016 13:14:12 -0600
changeset 320125 b7e672b68320b054ecc5d6ceca5d1870a6f44fdc
parent 319338 1ec3a3ff68f2d1a54e6ed33e926c28fee286bdf1
child 512691 8a9cc642d1516af3cc9eb410d0f1de962e41c728
push id9139
push userspolk@mozilla.com
push dateFri, 08 Jan 2016 19:22:08 +0000
reviewersted, cpearce
bugs1235606
milestone46.0a1
Bug 1235606 - Move firefox-media-tests into mozilla-central. r?ted, r?cpearce
dom/media/test/external-media-tests/MANIFEST.in
dom/media/test/external-media-tests/README.md
dom/media/test/external-media-tests/harness/__init__.py
dom/media/test/external-media-tests/harness/runtests.py
dom/media/test/external-media-tests/harness/testcase.py
dom/media/test/external-media-tests/media_tests/__init__.py
dom/media/test/external-media-tests/media_tests/manifest.ini
dom/media/test/external-media-tests/media_tests/playback/eme.ini
dom/media/test/external-media-tests/media_tests/playback/limiting_bandwidth.ini
dom/media/test/external-media-tests/media_tests/playback/manifest.ini
dom/media/test/external-media-tests/media_tests/playback/netflix_limiting_bandwidth.ini
dom/media/test/external-media-tests/media_tests/playback/test_eme_playback.py
dom/media/test/external-media-tests/media_tests/playback/test_full_playback.py
dom/media/test/external-media-tests/media_tests/playback/test_playback_limiting_bandwidth.py
dom/media/test/external-media-tests/media_tests/playback/test_ultra_low_bandwidth.py
dom/media/test/external-media-tests/media_tests/playback/test_video_playback.py
dom/media/test/external-media-tests/media_tests/playback/youtube/manifest.ini
dom/media/test/external-media-tests/media_tests/playback/youtube/test_basic_playback.py
dom/media/test/external-media-tests/media_tests/playback/youtube/test_prefs.py
dom/media/test/external-media-tests/media_tests/resources/mozilla.html
dom/media/test/external-media-tests/media_tests/test_example.py
dom/media/test/external-media-tests/media_tests/urls/default.ini
dom/media/test/external-media-tests/media_tests/urls/netflix/default.ini
dom/media/test/external-media-tests/media_tests/urls/youtube/archive/crash_videos.ini
dom/media/test/external-media-tests/media_tests/urls/youtube/archive/other_videos.ini
dom/media/test/external-media-tests/media_tests/urls/youtube/archive/video_data.ini
dom/media/test/external-media-tests/media_tests/urls/youtube/archive/youtube.ini
dom/media/test/external-media-tests/media_tests/urls/youtube/long1-720.ini
dom/media/test/external-media-tests/media_tests/urls/youtube/long2-720.ini
dom/media/test/external-media-tests/media_tests/urls/youtube/long3-crashes-720.ini
dom/media/test/external-media-tests/media_tests/urls/youtube/long4-crashes-900.ini
dom/media/test/external-media-tests/media_tests/urls/youtube/massive-6000.ini
dom/media/test/external-media-tests/media_tests/urls/youtube/medium1-60.ini
dom/media/test/external-media-tests/media_tests/urls/youtube/medium2-60.ini
dom/media/test/external-media-tests/media_tests/urls/youtube/medium3-120.ini
dom/media/test/external-media-tests/media_tests/urls/youtube/short0-10.ini
dom/media/test/external-media-tests/media_tests/urls/youtube/short1-15.ini
dom/media/test/external-media-tests/media_tests/urls/youtube/short2-15.ini
dom/media/test/external-media-tests/media_tests/urls/youtube/short3-crashes-15.ini
dom/media/test/external-media-tests/media_tests/utils.py
dom/media/test/external-media-tests/media_utils/__init__.py
dom/media/test/external-media-tests/media_utils/video_puppeteer.py
dom/media/test/external-media-tests/media_utils/youtube_puppeteer.py
dom/media/test/external-media-tests/requirements.txt
dom/media/test/external-media-tests/setup.py
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/MANIFEST.in
@@ -0,0 +1,7 @@
+exclude MANIFEST.in
+include requirements.txt
+include setup.py
+recursive-include firefox_media_tests *
+recursive-include media_utils *
+
+
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/README.md
@@ -0,0 +1,157 @@
+firefox-media-tests
+===================
+
+[Marionette Python tests][marionette-python-tests] for media playback in Mozilla Firefox. MediaTestCase uses [Firefox Puppeteer][ff-puppeteer-docs] library.
+
+Setup
+-----
+
+The instructions below assume you have a copy of the project in `some/path/firefox-media-tests` and they refer to this path as `$PROJECT_HOME`.
+
+* Create a virtualenv called `foo`.
+
+   ```sh
+   $ virtualenv foo
+   $ source foo/bin/activate #or `foo\Scripts\activate` on Windows
+   ```
+
+* Install `firefox-media-tests` in development mode. (To get an environment that is closer to what is actually used in Mozilla's automation jobs, run `pip install -r requirements.txt` first.)
+
+   ```sh
+   $ python setup.py develop
+   ```
+
+Now `firefox-media-tests` should be a recognized command. Try `firefox-media-tests --help` to see if it works.
+
+
+Running the Tests
+-----------------
+
+In the examples below, `$FF_PATH` is a path to a recent Firefox binary.
+
+This runs all the tests listed in `$PROJECT_HOME/firefox_media_tests/manifest.ini`:
+
+   ```sh
+   $ firefox-media-tests --binary $FF_PATH
+   ```
+
+You can also run all the tests at a particular path:
+
+   ```sh
+   $ firefox-media-tests --binary $FF_PATH some/path/foo
+   ```
+
+Or you can run the tests that are listed in a manifest file of your choice.
+
+   ```sh
+   $ firefox-media-tests --binary $FF_PATH some/other/path/manifest.ini
+   ```
+
+By default, the urls listed in `firefox_media_tests/urls/default.ini` are used for the tests, but you can also supply your own ini file of urls:
+
+   ```sh
+   $ firefox-media-tests --binary $FF_PATH --urls some/other/path/my_urls.ini
+   ```
+
+### Running EME tests
+
+In order to run EME tests, you must use a Firefox profile that has a signed plugin-container.exe and voucher.bin. With Netflix, this will be created when you log in and save the credentials. You must also use a custom .ini file for urls to the provider's content and indicate which test to run, like above. Ex:
+
+   ```sh
+   $ firefox-media-tests --binary $FF_PATH some/path/tests.ini --profile custom_profile --urls some/path/provider-urls.ini
+   ```
+
+
+### Running tests in a way that provides information about a crash
+
+What if Firefox crashes during a test run? You want to know why! To report useful crash data, the test runner needs access to a "minidump_stackwalk" binary and a "symbols.zip" file.
+
+1. Download a `minidump_stackwalk` binary for your platform (save it whereever). Get it from http://hg.mozilla.org/build/tools/file/tip/breakpad/.
+2. Make `minidump_stackwalk` executable
+
+   ```sh
+   $ chmod +x path/to/minidump_stackwalk
+   ```
+
+3. Create an environment variable called `MINIDUMP_STACKWALK` that points to that local path
+
+   ```sh
+   $ export MINIDUMP_STACKWALK=path/to/minidump_stackwalk
+   ```
+
+4. Download the `crashreporter-symbols.zip` file for the Firefox build you are testing and extract it. Example: ftp://ftp.mozilla.org/pub/firefox/tinderbox-builds/mozilla-aurora-win32/1427442016/firefox-38.0a2.en-US.win32.crashreporter-symbols.zip
+
+5. Run the tests with a `--symbols-path` flag
+
+  ```sh
+   $ firefox-media-tests --binary $FF_PATH --symbols-path path/to/example/firefox-38.0a2.en-US.win32.crashreporter-symbols
+  ```
+
+To check whether the above setup is working for you, trigger a (silly) Firefox crash while the tests are running. One way to do this is with the [crashme add-on](https://github.com/luser/crashme) -- you can add it to Firefox even while the tests are running. Another way on Linux and Mac OS systems:
+
+1. Find the process id (PID) of the Firefox process being used by the tests.
+
+  ```sh
+   $ ps x | grep 'Firefox'
+  ```
+
+2. Kill the Firefox process with SIGABRT.
+  ```sh
+  # 1234 is an example of a PID
+   $ kill -6 1234
+  ```
+
+Somewhere in the output produced by `firefox-media-tests`, you should see something like:
+
+```
+0:12.68 CRASH: MainThread pid:1234. Test:test_basic_playback.py TestVideoPlayback.test_playback_starts.
+Minidump anaylsed:False.
+Signature:[@ XUL + 0x2a65900]
+Crash dump filename:
+/var/folders/5k/xmn_fndx0qs2jcpcwhzl86wm0000gn/T/tmpB4Bolj.mozrunner/minidumps/DA3BB025-8302-4F96-8DF3-A97E424C877A.dmp
+Operating system: Mac OS X
+                  10.10.2 14C1514
+CPU: amd64
+     family 6 model 69 stepping 1
+     4 CPUs
+
+Crash reason:  EXC_SOFTWARE / SIGABRT
+Crash address: 0x104616900
+...
+```
+
+### Setting up for network shaping tests (browsermobproxy)
+
+1. Download the browsermob proxy zip file from http://bmp.lightbody.net/. The most current version as of this writing is browsermob-proxy-2.1.0-beta-2-bin.zip.
+2. Unpack the .zip file.
+3. Verify that you can launch browsermobproxy on your machine by running \<browsermob\>/bin/browsermob-proxy on your machine. I had to do a lot of work to install and use a java that browsermobproxy would like.
+4. Import the certificate into your Firefox profile. Select Preferences->Advanced->Certificates->View Certificates->Import... Navigate to <browsermob>/ssl-support and select cybervilliansCA.cer. Select all of the checkboxes.
+5. Tell marionette where browsermobproxy is and what port to start it on. Add the following command-line parameters to your firefox-media-tests command line:
+
+<pre><code>
+--browsermob-script <browsermob>/bin/browsermob-proxy --browsermob-port 999 --profile <your saved profile>
+</code></pre>
+
+On Windows, use browsermob-proxy.bat.
+
+You can then call browsermob to shape the network. You can find an example in firefox_media_tests/playback/test_playback_limiting_bandwidth.py. Another example can be found at https://dxr.mozilla.org/mozilla-central/source/testing/marionette/client/marionette/tests/unit/test_browsermobproxy.py.
+
+### A warning about video URLs
+The ini files in `firefox_media_tests/urls` may contain URLs pulled from Firefox crash or bug data. Automated tests don't care about video content, but you might: visit these at your own risk and be aware that they may be NSFW. We do not intend to ever moderate or filter these URLs.
+
+Writing a test
+--------------
+Write your test in a new or existing `test_*.py` file under `$PROJECT_HOME/firefox_media_tests`. Add it to the appropriate `manifest.ini` file(s) as well. Look in `media_utils` for useful video-playback functions.
+
+* [Marionette docs][marionette-docs]
+  - [Marionette Command Line Options](https://developer.mozilla.org/en-US/docs/Mozilla/Command_Line_Options)
+* [Firefox Puppeteer docs][ff-puppeteer-docs]
+
+License
+-------
+This software is licensed under the [Mozilla Public License v. 2.0](http://mozilla.org/MPL/2.0/).
+
+[marionette-python-tests]: https://developer.mozilla.org/en-US/docs/Mozilla/QA/Marionette/Marionette_Python_Tests
+[ff-puppeteer-docs]: http://firefox-puppeteer.readthedocs.org/en/latest/
+[marionette-docs]: http://marionette-client.readthedocs.org/en/latest/reference.html
+[ff-nightly]:https://nightly.mozilla.org/
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/harness/__init__.py
@@ -0,0 +1,5 @@
+# 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 runtests import cli
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/harness/runtests.py
@@ -0,0 +1,108 @@
+# 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 manifestparser import read_ini
+import os
+import sys
+
+from marionette import BaseMarionetteTestRunner, BaseMarionetteArguments
+from marionette.runner import BrowserMobProxyArguments
+from marionette.runtests import MarionetteHarness, cli as mn_cli
+import mozlog
+
+import media_tests
+from testcase import MediaTestCase
+from media_utils.video_puppeteer import debug_script
+
+
+class MediaTestArgumentsBase(object):
+    name = 'Firefox Media Tests'
+    args = [
+        [['--urls'], {
+            'help': 'ini file of urls to make available to all tests',
+            'default': os.path.join(media_tests.urls, 'default.ini'),
+        }],
+    ]
+
+    def verify_usage_handler(self, args):
+        if args.urls:
+           if not os.path.isfile(args.urls):
+               raise ValueError('--urls must provide a path to an ini file')
+           else:
+               path = os.path.abspath(args.urls)
+               args.video_urls = MediaTestArgumentsBase.get_urls(path)
+
+    def parse_args_handler(self, args):
+        if not args.tests:
+           args.tests = [media_tests.manifest]
+
+
+    @staticmethod
+    def get_urls(manifest):
+        with open(manifest, 'r'):
+            return [line[0] for line in read_ini(manifest)]
+
+
+class MediaTestArguments(BaseMarionetteArguments):
+    def __init__(self, **kwargs):
+        BaseMarionetteArguments.__init__(self, **kwargs)
+        self.register_argument_container(MediaTestArgumentsBase())
+        self.register_argument_container(BrowserMobProxyArguments())
+
+
+class MediaTestRunner(BaseMarionetteTestRunner):
+    def __init__(self, **kwargs):
+        BaseMarionetteTestRunner.__init__(self, **kwargs)
+        if not self.server_root:
+            self.server_root = media_tests.resources
+        # pick up prefs from marionette_driver.geckoinstance.DesktopInstance
+        self.app = 'fxdesktop'
+        self.test_handlers = [MediaTestCase]
+
+        # Used in HTML report (--log-html)
+        def gather_media_debug(test, status):
+            rv = {}
+            marionette = test._marionette_weakref()
+
+            if marionette.session is not None:
+                try:
+                    with marionette.using_context(marionette.CONTEXT_CHROME):
+                        debug_lines = marionette.execute_script(debug_script)
+                        if debug_lines:
+                            name = 'mozMediaSourceObject.mozDebugReaderData'
+                            rv[name] = '\n'.join(debug_lines)
+                        else:
+                            logger = mozlog.get_default_logger()
+                            logger.info('No data available about '
+                                        'mozMediaSourceObject')
+                except:
+                    logger = mozlog.get_default_logger()
+                    logger.warning('Failed to gather test failure media debug',
+                                   exc_info=True)
+            return rv
+
+        self.result_callbacks.append(gather_media_debug)
+
+
+class FirefoxMediaHarness(MarionetteHarness):
+    def __init__(self,
+                 runner_class=MediaTestRunner,
+                 parser_class=MediaTestArguments):
+        # workaround until next marionette-client release - Bug 1227918
+        try:
+            MarionetteHarness.__init__(self, runner_class, parser_class)
+        except Exception:
+            logger = mozlog.commandline.setup_logging('Media-test harness', {})
+            logger.error('Failure setting up harness', exc_info=True)
+            raise
+
+    def parse_args(self, *args, **kwargs):
+        return MarionetteHarness.parse_args(self, {'mach': sys.stdout})
+
+
+def cli():
+    mn_cli(MediaTestRunner, MediaTestArguments, FirefoxMediaHarness)
+
+if __name__ == '__main__':
+    cli()
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/harness/testcase.py
@@ -0,0 +1,138 @@
+# 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
+
+from marionette import BrowserMobProxyTestCaseMixin
+from marionette_driver import Wait
+from marionette_driver.errors import TimeoutException
+from marionette.marionette_test import SkipTest
+
+from firefox_puppeteer.testcases import FirefoxTestCase
+from media_tests.utils import (timestamp_now, verbose_until)
+from media_utils.video_puppeteer import (playback_done, playback_started,
+                                         VideoException, VideoPuppeteer as VP)
+
+
+class MediaTestCase(FirefoxTestCase):
+
+    def __init__(self, *args, **kwargs):
+        self.video_urls = kwargs.pop('video_urls', False)
+        FirefoxTestCase.__init__(self, *args, **kwargs)
+
+    def save_screenshot(self):
+        screenshot_dir = os.path.join(self.marionette.instance.workspace or '',
+                                      'screenshots')
+        filename = ''.join([self.id().replace(' ', '-'),
+                            '_',
+                            str(timestamp_now()),
+                            '.png'])
+        path = os.path.join(screenshot_dir, filename)
+        if not os.path.exists(screenshot_dir):
+            os.makedirs(screenshot_dir)
+        with self.marionette.using_context('content'):
+            img_data = self.marionette.screenshot()
+        with open(path, 'wb') as f:
+            f.write(img_data.decode('base64'))
+        self.marionette.log('Screenshot saved in %s' % os.path.abspath(path))
+
+    def log_video_debug_lines(self):
+        with self.marionette.using_context('chrome'):
+            debug_lines = self.marionette.execute_script(VP._debug_script)
+            if debug_lines:
+                self.marionette.log('\n'.join(debug_lines))
+
+    def run_playback(self, video):
+        with self.marionette.using_context('content'):
+            self.logger.info(video.test_url)
+            try:
+                verbose_until(Wait(video, interval=video.interval,
+                                   timeout=video.expected_duration * 1.3 +
+                                   video.stall_wait_time),
+                              video, playback_done)
+            except VideoException as e:
+                raise self.failureException(e)
+
+    def check_playback_starts(self, video):
+        with self.marionette.using_context('content'):
+            self.logger.info(video.test_url)
+            try:
+                verbose_until(Wait(video, timeout=video.timeout),
+                              video, playback_started)
+            except TimeoutException as e:
+                raise self.failureException(e)
+
+    def skipTest(self, reason):
+        """
+        Skip this test.
+
+        Skip with marionette.marionette_test import SkipTest so that it
+        gets recognized a skip in marionette.marionette_test.CommonTestCase.run
+        """
+        raise SkipTest(reason)
+
+
+class NetworkBandwidthTestCase(MediaTestCase):
+
+    def __init__(self, *args, **kwargs):
+        MediaTestCase.__init__(self, *args, **kwargs)
+        BrowserMobProxyTestCaseMixin.__init__(self, *args, **kwargs)
+        self.proxy = None
+
+    def setUp(self):
+        MediaTestCase.setUp(self)
+        BrowserMobProxyTestCaseMixin.setUp(self)
+        self.proxy = self.create_browsermob_proxy()
+
+    def tearDown(self):
+        MediaTestCase.tearDown(self)
+        BrowserMobProxyTestCaseMixin.tearDown(self)
+        self.proxy = None
+
+
+    def run_videos(self):
+        with self.marionette.using_context('content'):
+            for url in self.video_urls:
+                video = VP(self.marionette, url,
+                                       stall_wait_time=60,
+                                       set_duration=60)
+                self.run_playback(video)
+
+
+class VideoPlaybackTestsMixin(object):
+
+    """ 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 test both starting videos and performing partial playback at one
+    minute each, and is the test that should be run frequently in automation.
+    """
+
+    def test_playback_starts(self):
+        with self.marionette.using_context('content'):
+            for url in self.video_urls:
+                try:
+                    video = VP(self.marionette, url, timeout=60)
+                    # Second playback_started check in case video._start_time
+                    # is not 0
+                    self.check_playback_starts(video)
+                    video.pause()
+                    src = video.video_src
+                    if not src.startswith('mediasource'):
+                        self.marionette.log('video is not '
+                                            'mediasource: %s' % src,
+                                            level='WARNING')
+                except TimeoutException as e:
+                    raise self.failureException(e)
+
+    def test_video_playback_partial(self):
+        """ First 60 seconds of video play well. """
+        with self.marionette.using_context('content'):
+            for url in self.video_urls:
+                video = VP(self.marionette, url,
+                           stall_wait_time=10,
+                           set_duration=60)
+                self.run_playback(video)
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/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')
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/media_tests/manifest.ini
@@ -0,0 +1,1 @@
+[include:playback/manifest.ini]
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/media_tests/playback/eme.ini
@@ -0,0 +1,1 @@
+[test_eme_playback.py]
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/media_tests/playback/limiting_bandwidth.ini
@@ -0,0 +1,2 @@
+[test_playback_limiting_bandwidth.py]
+[test_ultra_low_bandwidth.py]
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/media_tests/playback/manifest.ini
@@ -0,0 +1,1 @@
+[test_video_playback.py]
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/media_tests/playback/netflix_limiting_bandwidth.ini
@@ -0,0 +1,1 @@
+[test_playback_limiting_bandwidth.py]
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/media_tests/playback/test_eme_playback.py
@@ -0,0 +1,71 @@
+# 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 harness.testcase import MediaTestCase, VideoPlaybackTestsMixin
+
+
+class TestEMEPlayback(MediaTestCase, VideoPlaybackTestsMixin):
+
+    def setUp(self):
+        super(TestEMEPlayback, self).setUp()
+        self.set_eme_prefs()
+        assert(self.check_eme_prefs())
+
+    def set_eme_prefs(self):
+        with self.marionette.using_context('chrome'):
+
+            # https://bugzilla.mozilla.org/show_bug.cgi?id=1187471#c28
+            # 2015-09-28 cpearce says this is no longer necessary, but in case
+            # we are working with older firefoxes...
+            self.prefs.set_pref('media.gmp.trial-create.enabled', False)
+
+    def check_and_log_boolean_pref(self, pref_name, expected_value):
+        with self.marionette.using_context('chrome'):
+            pref_value = self.prefs.get_pref(pref_name)
+
+            if pref_value is None:
+                self.logger.info('Pref %s has no value.' % pref_name)
+                return False
+            else:
+                self.logger.info('Pref %s = %s' % (pref_name, pref_value))
+                if pref_value != expected_value:
+                    self.logger.info('Pref %s has unexpected value.'
+                                     % pref_name)
+                    return False
+
+        return True
+
+    def check_and_log_integer_pref(self, pref_name, minimum_value=0):
+        with self.marionette.using_context('chrome'):
+            pref_value = self.prefs.get_pref(pref_name)
+
+            if pref_value is None:
+                self.logger.info('Pref %s has no value.' % pref_name)
+                return False
+            else:
+                self.logger.info('Pref %s = %s' % (pref_name, pref_value))
+
+                match = re.search('^\d+$', pref_value)
+                if not match:
+                    self.logger.info('Pref %s is not an integer' % pref_name)
+                    return False
+
+            return pref_value >= minimum_value
+
+    def check_eme_prefs(self):
+        with self.marionette.using_context('chrome'):
+            prefs_ok = self.check_and_log_boolean_pref(
+                'media.mediasource.enabled', True)
+            prefs_ok = self.check_and_log_boolean_pref(
+                'media.eme.enabled', True) and prefs_ok
+            prefs_ok = self.check_and_log_boolean_pref(
+                'media.mediasource.mp4.enabled', True) and prefs_ok
+            prefs_ok = self.check_and_log_boolean_pref(
+                'media.gmp-eme-adobe.enabled', True) and prefs_ok
+            prefs_ok = self.check_and_log_integer_pref(
+                'media.gmp-eme-adobe.version', 1) and prefs_ok
+
+        return prefs_ok
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/media_tests/playback/test_full_playback.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 harness.testcase import MediaTestCase
+from 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('content'):
+            for url in self.video_urls:
+                video = VideoPuppeteer(self.marionette, url,
+                                       stall_wait_time=10)
+                self.run_playback(video)
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/media_tests/playback/test_playback_limiting_bandwidth.py
@@ -0,0 +1,23 @@
+# 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 import BrowserMobProxyTestCaseMixin
+
+from harness.testcase import NetworkBandwidthTestCase
+
+
+class TestPlaybackLimitingBandwidth(NetworkBandwidthTestCase,
+                                    BrowserMobProxyTestCaseMixin):
+
+    def test_playback_limiting_bandwidth_250(self):
+        self.proxy.limits({'downstream_kbps': 250})
+        self.run_videos()
+
+    def test_playback_limiting_bandwidth_500(self):
+        self.proxy.limits({'downstream_kbps': 500})
+        self.run_videos()
+
+    def test_playback_limiting_bandwidth_1000(self):
+        self.proxy.limits({'downstream_kbps': 1000})
+        self.run_videos()
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/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 import BrowserMobProxyTestCaseMixin
+
+from harness.testcase import NetworkBandwidthTestCase
+
+
+class TestUltraLowBandwidth(NetworkBandwidthTestCase,
+                                    BrowserMobProxyTestCaseMixin):
+
+    def test_playback_limiting_bandwidth_160(self):
+        self.proxy.limits({'downstream_kbps': 160})
+        self.run_videos()
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/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 harness.testcase import (
+    MediaTestCase,
+    VideoPlaybackTestsMixin
+)
+
+
+class TestVideoPlayback(MediaTestCase, VideoPlaybackTestsMixin):
+
+    # Tests are actually implemented in VideoPlaybackTestsMixin.
+
+    pass
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/media_tests/playback/youtube/manifest.ini
@@ -0,0 +1,1 @@
+[test_basic_playback.py ]
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/media_tests/playback/youtube/test_basic_playback.py
@@ -0,0 +1,73 @@
+# 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 media_tests.utils import verbose_until
+from harness.testcase import MediaTestCase
+from media_utils.video_puppeteer import VideoException
+from media_utils.youtube_puppeteer import (YouTubePuppeteer, playback_done,
+                                           wait_for_almost_done)
+
+
+class TestBasicYouTubePlayback(MediaTestCase):
+    def test_mse_is_enabled_by_default(self):
+        with self.marionette.using_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,
+                              lambda y: y.video_src.startswith('mediasource'),
+                              "Failed to find 'mediasource' 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('content'):
+            for url in self.video_urls:
+                self.logger.info(url)
+                youtube = YouTubePuppeteer(self.marionette, url)
+                self.logger.info('Expected duration: %s' %
+                                 youtube.expected_duration)
+                youtube.deactivate_autoplay()
+
+                final_piece = 60
+                try:
+                    time_left = wait_for_almost_done(youtube,
+                                                     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: %s - %s seconds left.' %
+                                     (youtube.movie_id, time_left))
+                    if time_left > final_piece:
+                        self.marionette.log('time_left greater than '
+                                            'final_piece - %s' % time_left,
+                                            level='WARNING')
+                        self.save_screenshot()
+                else:
+                    self.marionette.log('Duration close to 0 - %s' % youtube,
+                                        level='WARNING')
+                    self.save_screenshot()
+                try:
+                    verbose_until(Wait(youtube,
+                                       timeout=max(100, time_left) * 1.3,
+                                       interval=1),
+                                  youtube,
+                                  playback_done)
+                except TimeoutException as e:
+                    raise self.failureException(e)
+
+    def test_playback_starts(self):
+        with self.marionette.using_context('content'):
+            for url in self.video_urls:
+                try:
+                    YouTubePuppeteer(self.marionette, url, timeout=60)
+                except TimeoutException as e:
+                    raise self.failureException(e)
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/media_tests/playback/youtube/test_prefs.py
@@ -0,0 +1,49 @@
+# 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 harness.testcase import MediaTestCase
+from marionette_driver import Wait
+
+from media_tests.utils import verbose_until
+from 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_src('http', self.test_urls[0])
+
+        self.set_mse_enabled_prefs(True)
+        self.check_src('mediasource', self.test_urls[0])
+
+    def set_mse_enabled_prefs(self, value):
+        with self.marionette.using_context('chrome'):
+            self.prefs.set_pref('media.mediasource.enabled', value)
+            self.prefs.set_pref('media.mediasource.mp4.enabled', value)
+
+    def check_src(self, src_type, url):
+        # Why wait to check src until initial ad is done playing?
+        # - src attribute in video element is sometimes null during ad playback
+        # - many ads still don't use MSE even if main video does
+        with self.marionette.using_context('content'):
+            youtube = YouTubePuppeteer(self.marionette, url)
+            youtube.attempt_ad_skip()
+            wait = Wait(youtube,
+                        timeout=min(self.max_timeout,
+                                    youtube.player_duration * 1.3),
+                        interval=1)
+
+            def cond(y):
+                return y.video_src.startswith(src_type)
+
+            verbose_until(wait, youtube, cond)
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/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>
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/media_tests/test_example.py
@@ -0,0 +1,19 @@
+from 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('content'):
+            self.marionette.navigate(self.test_urls[0])
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/media_tests/urls/default.ini
@@ -0,0 +1,9 @@
+# short videos; no ads; max 5 minutes
+# 0:12
+[https://youtu.be/AbAACm1IQE0]
+# 2:18
+[https://www.youtube.com/watch?v=yOQQCoxs8-k]
+# 0:08
+[https://www.youtube.com/watch?v=1visYpIREUM]
+# 2:09
+[https://www.youtube.com/watch?v=rjmuKV9BTkE]
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/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]
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/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]
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/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]
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/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]
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/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]
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/media_tests/urls/youtube/long1-720.ini
@@ -0,0 +1,14 @@
+# all long videos; < 12 hours total
+# 2:18:00
+[http://youtu.be/FLX64H5FYa8]
+# 1:00:00
+[https://www.youtube.com/watch?v=AYYDshv8C4g]
+# 1:10:00
+[https://www.youtube.com/watch?v=V0Vy4kYAPDk]
+# 1:47:00
+[https://www.youtube.com/watch?v=bFtGE2C7Pxs]
+
+
+# shutdownhang | WaitForSingleObjectEx | WaitForSingleObject | PR_Wait | nsThread::ProcessNextEvent(bool, bool*) | NS_ProcessNextEvent(nsIThread*, bool) | mozilla::MediaShutdownManager::Shutdown()
+# 1:43:00
+[https://www.youtube.com/watch?v=BXMtXpmpXPU]
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/media_tests/urls/youtube/long2-720.ini
@@ -0,0 +1,9 @@
+# a couple of very long videos, < 12 hours total
+# 6:00:00
+[https://www.youtube.com/watch?v=5N8sUccRiTA]
+# 2:27:00
+[https://www.youtube.com/watch?v=NAVrm3wjzq8]
+# 58:50
+[https://www.youtube.com/watch?v=uP1BBw3IYco]
+# 2:09:00
+[https://www.youtube.com/watch?v=b6q5N16dje4]
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/media_tests/urls/youtube/long3-crashes-720.ini
@@ -0,0 +1,36 @@
+# 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]
+
+
+
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/media_tests/urls/youtube/long4-crashes-900.ini
@@ -0,0 +1,83 @@
+# 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]
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/media_tests/urls/youtube/massive-6000.ini
@@ -0,0 +1,15 @@
+# very long test; 96-100 hours?
+# 00:3:26
+[https://www.youtube.com/watch?v=7RMQksXpQSk]
+# nyan cat 10 hours
+[http://youtu.be/9bZkp7q19f0]
+# 4:54:00
+[https://www.youtube.com/watch?v=jWlKjw3LBDk]
+# 3:00:01
+[https://www.youtube.com/watch?v=ub9JUDS_6i8]
+# 10 hours rick roll
+[https://www.youtube.com/watch?v=BROWqjuTM0g]
+# 24 hours
+[https://www.youtube.com/watch?v=FvHiLLkPhQE]
+# 2 hours
+[https://www.youtube.com/watch?v=VmOuW5zTt9w
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/media_tests/urls/youtube/medium1-60.ini
@@ -0,0 +1,18 @@
+# mix of shorter/longer videos with/without ads, < 60 min
+# 4:59
+[http://youtu.be/pWI8RB2dmfU]
+# 0:46 ad at start
+[http://youtu.be/6SFp1z7uA6g]
+# 0:58 ad at start
+[http://youtu.be/Aebs62bX0dA]
+# 1:43 ad
+[https://www.youtube.com/watch?v=l5ODwR6FPRQ]
+# 8:00 ad
+[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/watch?v=cht9Xq9suGg]
+# 1:35 ad
+[https://www.youtube.com/watch?v=orybDrUj4vA]
+# 3:02 - ad
+[https://youtu.be/tDDVAErOI5U]
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/media_tests/urls/youtube/medium2-60.ini
@@ -0,0 +1,6 @@
+# a few longer videos, < 60 min total
+# 0:30:00 no ad
+[https://www.youtube.com/watch?v=-qXxNPvqHtQ]
+# 0:20:00
+[http://youtu.be/Fu2DcHzokew]
+
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/media_tests/urls/youtube/medium3-120.ini
@@ -0,0 +1,11 @@
+# a few longer videos, < 120 min total
+# video with ad in the middle
+# 21:00
+[https://www.youtube.com/watch?v=cht9Xq9suGg]
+# 16:00
+[https://www.youtube.com/watch?v=6Lm9EHhbJAY]
+# 20:00
+[https://www.youtube.com/watch?v=8XQ1onjXJK0]
+# 59:06
+[https://www.youtube.com/watch?v=kmpiY5kssU4]
+
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/media_tests/urls/youtube/short0-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]
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/media_tests/urls/youtube/short1-15.ini
@@ -0,0 +1,5 @@
+# 00:12
+[https://youtu.be/AbAACm1IQE0]
+# longer video with ads; < 15 min total
+# 13:40
+[https://www.youtube.com/watch?v=87uo2TPrsl8]
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/media_tests/urls/youtube/short2-15.ini
@@ -0,0 +1,5 @@
+# 1-2 longer videos with ads; < 15 minutes total
+[https://www.youtube.com/watch?v=v678Em6qyzk]
+[https://www.youtube.com/watch?v=l8XOZJkozfI]
+
+
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/media_tests/urls/youtube/short3-crashes-15.ini
@@ -0,0 +1,14 @@
+# 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]
+
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/media_tests/utils.py
@@ -0,0 +1,66 @@
+# 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.
+    """
+    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)
new file mode 100644
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/media_utils/video_puppeteer.py
@@ -0,0 +1,254 @@
+# 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 time import clock, sleep
+
+from marionette_driver import By, expected, Wait
+
+from 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 property is called.
+    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.
+
+    Inputs:
+        marionette - The marionette instance this runs in.
+        url - the URL of the page containing the video element.
+        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.
+        interval - The polling interval that is used to check progress.
+        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.
+        stall_wait_time - The amount of time to wait to see if a stall has
+            cleared. If 0, do not check for stalls.
+        timeout - The amount of time to wait until the video starts.
+    """
+    def __init__(self, marionette, url, video_selector='video', interval=1,
+                 set_duration=0, stall_wait_time=0, timeout=60):
+        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._start_time = 0
+        self._start_wall_time = 0
+        wait = Wait(self.marionette, timeout=self.timeout)
+        with self.marionette.using_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');")
+            # To get an accurate expected_duration, playback must have started
+            wait = Wait(self, timeout=self.timeout)
+            verbose_until(wait, self, lambda v: v.current_time > 0,
+                          "Check if video current_time > 0")
+            self._start_time = self.current_time
+            self._start_wall_time = clock()
+            self.update_expected_duration()
+
+    def update_expected_duration(self):
+        """
+        Update the duration of the target video at self.test_url (in seconds).
+
+        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.duration is the duration of whatever is playing right now.
+        # 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.duration is the duration of the main
+        # video.
+        video_duration = self.duration
+        set_duration = self._set_duration
+        # In case video starts at t > 0, adjust target time partial playback
+        if self._set_duration and self._start_time:
+            set_duration += self._start_time
+        if 0 < set_duration < video_duration:
+            self.expected_duration = set_duration
+        else:
+            self.expected_duration = video_duration
+
+    def get_debug_lines(self):
+        with self.marionette.using_context('chrome'):
+            debug_lines = self.marionette.execute_script(debug_script)
+        return debug_lines
+
+    def play(self):
+        self.execute_video_script('arguments[0].wrappedJSObject.play();')
+
+    def pause(self):
+        self.execute_video_script('arguments[0].wrappedJSObject.pause();')
+
+    @property
+    def duration(self):
+        """
+        Return duration in seconds of whatever stream is playing right now.
+        """
+        return self.execute_video_script('return arguments[0].'
+                                         'wrappedJSObject.duration;') or 0
+
+    @property
+    def current_time(self):
+        """
+        Return current time of whatever stream is playing right now.
+        """
+        return self.execute_video_script(
+            'return arguments[0].wrappedJSObject.currentTime;') or 0
+
+    @property
+    def remaining_time(self):
+        # Note that self.current_time could temporarily refer to a
+        # spliced-in ad
+        return self.expected_duration - self.current_time
+
+    @property
+    def video_src(self):
+        with self.marionette.using_context('content'):
+            return self.video.get_attribute('src')
+
+    @property
+    def total_frames(self):
+        return self.execute_video_script("""
+            return arguments[0].getVideoPlaybackQuality()["totalVideoFrames"];
+            """)
+
+    @property
+    def dropped_frames(self):
+        return self.execute_video_script("""return
+            arguments[0].getVideoPlaybackQuality()["droppedVideoFrames"];
+            """) or 0
+
+    @property
+    def corrupted_frames(self):
+        return self.execute_video_script("""return
+            arguments[0].getVideoPlaybackQuality()["corruptedVideoFrames"];
+            """) or 0
+
+    @property
+    def video_url(self):
+        return self.execute_video_script('return arguments[0].baseURI;')
+
+    @property
+    def lag(self):
+        # Note that self.current_time could temporarily refer to a
+        # spliced-in ad
+        elapsed_current_time = self.current_time - self._start_time
+        elapsed_wall_time = clock() - self._start_wall_time
+        return elapsed_wall_time - elapsed_current_time
+
+    def measure_progress(self):
+        initial = self.current_time
+        sleep(1)
+        return self.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
+        `arguments[0]` in script refers to video element.
+        :return: value returned by script
+        """
+        with self.marionette.using_context('content'):
+            return self.marionette.execute_script(script,
+                                                  script_args=[self.video])
+
+    def __str__(self):
+        messages = ['%s - test url: %s: {' % (type(self).__name__,
+                                              self.test_url)]
+        if self.video:
+            messages += [
+                '\t(video)',
+                '\tcurrent_time: {0},'.format(self.current_time),
+                '\tduration: {0},'.format(self.duration),
+                '\texpected_duration: {0},'.format(self.expected_duration),
+                '\tlag: {0},'.format(self.lag),
+                '\turl: {0}'.format(self.video_url),
+                '\tsrc: {0}'.format(self.video_src),
+                '\tframes total: {0}'.format(self.total_frames),
+                '\t - dropped: {0}'.format(self.dropped_frames),
+                '\t - corrupted: {0}'.format(self.corrupted_frames)
+            ]
+        else:
+            messages += ['\tvideo: None']
+        messages.append('}')
+        return '\n'.join(messages)
+
+
+class VideoException(Exception):
+    pass
+
+
+def playback_started(video):
+    try:
+        return video.current_time > video._start_time
+    except Exception as e:
+        print ('Got exception %s' % e)
+        return False
+
+
+def playback_done(video):
+    # 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.
+    remaining_time = video.remaining_time
+    if abs(remaining_time) < video.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 video.stall_wait_time and (video.lag > video.stall_wait_time):
+        raise VideoException('Video %s stalled.\n%s' % (video.video_url,
+                                                        video))
+
+    # We are cruising, so we are not done.
+    return False
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/media_utils/youtube_puppeteer.py
@@ -0,0 +1,451 @@
+# 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 time import sleep
+import re
+from json import loads
+
+from marionette_driver import By, expected, Wait
+from marionette_driver.errors import TimeoutException, NoSuchElementException
+from video_puppeteer import VideoPuppeteer, VideoException
+from media_tests.utils import verbose_until
+
+
+class YouTubePuppeteer(VideoPuppeteer):
+    """
+    Wrapper around a YouTube #movie_player element
+
+    Partial reference: https://developers.google.com/youtube/js_api_reference
+    """
+
+    _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, **kwargs):
+        self.player = None
+        super(YouTubePuppeteer,
+              self).__init__(marionette, url,
+                             video_selector='#movie_player video',
+                             **kwargs)
+        wait = Wait(self.marionette, timeout=30)
+        with self.marionette.using_context('content'):
+            verbose_until(wait, self,
+                          expected.element_present(By.ID, 'movie_player'))
+            self.player = self.marionette.find_element(By.ID, 'movie_player')
+            self.marionette.execute_script("log('#movie_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.ad_inactive and self.duration and not
+                    self.player_buffering):
+                break
+        self.update_expected_duration()
+
+    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();')
+
+    @property
+    def player_duration(self):
+        """ Duration in seconds via YouTube API.
+        """
+        return self.execute_yt_script('return arguments[1].'
+                                      'wrappedJSObject.getDuration();')
+
+    @property
+    def player_current_time(self):
+        """ Current time in seconds via YouTube API.
+        """
+        return self.execute_yt_script('return arguments[1].'
+                                      'wrappedJSObject.getCurrentTime();')
+
+    @property
+    def player_remaining_time(self):
+        """ Remaining time in seconds via YouTube API.
+        """
+        return self.expected_duration - self.player_current_time
+
+    def player_measure_progress(self):
+        """ Playback progress in seconds via YouTube API.
+        """
+        initial = self.player_current_time
+        sleep(1)
+        return self.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 #movie_player element.
+        :param script: script to be executed.
+        `arguments[0]` in script refers to video element, `arguments[1]` refers
+        to #movie_player element (YouTube).
+        :return: value returned by script
+        """
+        with self.marionette.using_context('content'):
+            return self.marionette.execute_script(script,
+                                                  script_args=[self.video,
+                                                               self.player])
+
+    @property
+    def playback_quality(self):
+        return self.execute_yt_script('return arguments[1].'
+                                      'wrappedJSObject.getPlaybackQuality();')
+
+    @property
+    def movie_id(self):
+        return self.execute_yt_script('return arguments[1].'
+                                      'wrappedJSObject.'
+                                      'getVideoData()["video_id"];')
+
+    @property
+    def movie_title(self):
+        title = self.execute_yt_script('return arguments[1].'
+                                       'wrappedJSObject.'
+                                       'getVideoData()["title"];')
+        # title may include non-ascii characters; replace them to avoid
+        # UnicodeEncodeError in string formatting for log messages
+        return title.encode('ascii', 'replace')
+
+    @property
+    def player_url(self):
+        return self.execute_yt_script('return arguments[1].'
+                                      'wrappedJSObject.getVideoUrl();')
+
+    @property
+    def player_state(self):
+        state = self.execute_yt_script('return arguments[1].'
+                                       'wrappedJSObject.getPlayerState();')
+        return state
+
+    @property
+    def player_unstarted(self):
+        return self.player_state == self._yt_player_state['UNSTARTED']
+
+    @property
+    def player_ended(self):
+        return self.player_state == self._yt_player_state['ENDED']
+
+    @property
+    def player_playing(self):
+        return self.player_state == self._yt_player_state['PLAYING']
+
+    @property
+    def player_paused(self):
+        return self.player_state == self._yt_player_state['PAUSED']
+
+    @property
+    def player_buffering(self):
+        return self.player_state == self._yt_player_state['BUFFERING']
+
+    @property
+    def player_cued(self):
+        return self.player_state == self._yt_player_state['CUED']
+
+    @property
+    def ad_state(self):
+        # Note: ad_state is sometimes not accurate, due to some sort of lag?
+        return self.execute_yt_script('return arguments[1].'
+                                      'wrappedJSObject.getAdState();')
+
+    @property
+    def ad_format(self):
+        """
+        When ad is not playing, ad_format is False.
+
+        :return: integer representing ad format, or False
+        """
+        state = self.get_ad_displaystate()
+        ad_format = False
+        if state:
+            with self.marionette.using_context('content'):
+                ad_format = self.marionette.execute_script("""
+                    return arguments[0].adFormat;""",
+                    script_args=[state])
+        return ad_format
+
+    @property
+    def ad_skippable(self):
+        state = self.get_ad_displaystate()
+        skippable = False
+        if state:
+            with self.marionette.using_context('content'):
+                skippable = self.marionette.execute_script("""
+                    return arguments[0].skippable;""",
+                    script_args=[state])
+        return skippable
+
+    def get_ad_displaystate(self):
+        # may return None
+        return self.execute_yt_script('return arguments[1].'
+                                      'wrappedJSObject.'
+                                      'getOption("ad", "displaystate");')
+
+    @property
+    def breaks_count(self):
+        """
+        Number of upcoming ad breaks.
+        """
+        breaks = self.execute_yt_script('return arguments[1].'
+                                        'wrappedJSObject.'
+                                        'getOption("ad", "breakscount")')
+        # if video is not associated with any ads, breaks will be null
+        return breaks or 0
+
+    @property
+    def ad_inactive(self):
+        return (self.ad_ended or
+                self.ad_state == self._yt_player_state['UNSTARTED'])
+
+    @property
+    def ad_playing(self):
+        return self.ad_state == self._yt_player_state['PLAYING']
+
+    @property
+    def ad_ended(self):
+        return self.ad_state == self._yt_player_state['ENDED']
+
+    def process_ad(self):
+        if self.attempt_ad_skip() or self.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 s for ad' % ad_timeout)
+            verbose_until(wait, self, lambda y: y.ad_ended, "Check if ad ended")
+        except TimeoutException:
+            self.marionette.log('Waiting for ad to end timed out',
+                                level='WARNING')
+
+    def attempt_ad_skip(self):
+        """
+        Attempt to skip ad by clicking on skip-add button.
+        Return True if clicking of ad-skip button occurred.
+        """
+        # Wait for ad to load and become skippable
+        if self.ad_playing:
+            self.marionette.log('Waiting while ad plays')
+            sleep(10)
+        else:
+            # no ad playing
+            return False
+        if self.ad_skippable:
+            selector = '#movie_player .videoAdUiSkipContainer'
+            wait = Wait(self.marionette, timeout=30)
+            try:
+                with self.marionette.using_context('content'):
+                    wait.until(expected.element_displayed(By.CSS_SELECTOR,
+                                                          selector))
+                    ad_button = self.marionette.find_element(By.CSS_SELECTOR,
+                                                             selector)
+                    ad_button.click()
+                    self.marionette.log('Skipped ad.')
+                    return True
+            except (TimeoutException, NoSuchElementException):
+                self.marionette.log('Could not obtain '
+                                    'element: %s' % selector,
+                                    level='WARNING')
+        return False
+
+    def search_ad_duration(self):
+        """
+        :return: ad duration in seconds, if currently displayed in player
+        """
+        if not (self.ad_playing or self.player_measure_progress() == 0):
+            return None
+        # If the ad is not Flash...
+        if (self.ad_playing and self.video_src.startswith('mediasource') and
+                self.duration):
+            return self.duration
+        selector = '#movie_player .videoAdUiAttribution'
+        wait = Wait(self.marionette, timeout=5)
+        try:
+            with self.marionette.using_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: %s' % selector,
+                                level='WARNING')
+        return None
+
+    @property
+    def player_stalled(self):
+        """
+        :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.ad_playing and
+                    self.measure_progress() < 0.1 and
+                    self.player_measure_progress() < 0.1 and
+                    (self.player_playing or self.player_buffering))
+
+        if condition():
+            sleep(2)
+            if self.player_buffering:
+                sleep(5)
+            return condition()
+        else:
+            return False
+
+    def deactivate_autoplay(self):
+        """
+        Attempt to turn off autoplay. Return True if successful.
+        """
+        element_id = 'autoplay-checkbox'
+        mn = self.marionette
+        wait = Wait(mn, timeout=10)
+
+        def get_status(el):
+            script = 'return arguments[0].wrappedJSObject.checked'
+            return mn.execute_script(script, script_args=[el])
+
+        try:
+            with mn.using_context('content'):
+                # the width, height of the element are 0, so it's not visible
+                wait.until(expected.element_present(By.ID, element_id))
+                checkbox = mn.find_element(By.ID, element_id)
+
+                # Note: in some videos, due to late-loading of sidebar ads, the
+                # button is rerendered after sidebar ads appear & the autoplay
+                # pref resets to "on". In other words, if you click too early,
+                # the pref might get reset moments later.
+                sleep(1)
+                if get_status(checkbox):
+                    mn.execute_script('return arguments[0].'
+                                      'wrappedJSObject.click()',
+                                      script_args=[checkbox])
+                    self.marionette.log('Toggled autoplay.')
+                autoplay = get_status(checkbox)
+                self.marionette.log('Autoplay is %s' % autoplay)
+                return (autoplay is not None) and (not autoplay)
+        except (NoSuchElementException, TimeoutException):
+            return False
+
+    def __str__(self):
+        messages = [super(YouTubePuppeteer, self).__str__()]
+        if self.player:
+            player_state = self._yt_player_state_name[self.player_state]
+            ad_state = self._yt_player_state_name[self.ad_state]
+            messages += [
+                '#movie_player: {',
+                '\tvideo id: {0},'.format(self.movie_id),
+                '\tvideo_title: {0}'.format(self.movie_title),
+                '\tcurrent_state: {0},'.format(player_state),
+                '\tad_state: {0},'.format(ad_state),
+                '\tplayback_quality: {0},'.format(self.playback_quality),
+                '\tcurrent_time: {0},'.format(self.player_current_time),
+                '\tduration: {0},'.format(self.player_duration),
+                '}'
+            ]
+        else:
+            messages += ['\t#movie_player: None']
+        return '\n'.join(messages)
+
+
+def playback_started(yt):
+    """
+    Check whether playback has started.
+    :param yt: YouTubePuppeteer
+    """
+    # usually, ad is playing during initial buffering
+    if (yt.player_state in
+            [yt._yt_player_state['PLAYING'],
+             yt._yt_player_state['BUFFERING']]):
+        return True
+    if yt.current_time > 0 or yt.player_current_time > 0:
+        return True
+    return False
+
+
+def playback_done(yt):
+    """
+    Check whether playback is done, skipping ads if possible.
+    :param yt: YouTubePuppeteer
+    """
+    # in case ad plays at end of video
+    if yt.ad_state == yt._yt_player_state['PLAYING']:
+        yt.attempt_ad_skip()
+        return False
+    return yt.player_ended or yt.player_remaining_time < 1
+
+
+def wait_for_almost_done(yt, 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.
+
+    :param yt: YouTubePuppeteer
+    """
+    rest = 10
+    duration = remaining_time = yt.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 yt.player_stalled:
+            if yt.player_buffering:
+                # fall back on timeout in 'wait' call that comes after this
+                # in test function
+                yt.marionette.log('Buffering and no playback progress.')
+                break
+            else:
+                message = '\n'.join(['Playback stalled', str(yt)])
+                raise VideoException(message)
+        if yt.breaks_count > 0:
+            yt.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 = yt.player_remaining_time
+    return remaining_time
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/requirements.txt
@@ -0,0 +1,22 @@
+browsermob-proxy==0.6.0
+manifestparser==1.1
+mozcrash==0.16
+mozdevice==0.46
+mozfile==1.2
+mozhttpd==0.7
+mozinfo==0.8
+# optional - mozharness install step
+mozInstall==1.12
+mozlog==3.0
+moznetwork==0.27
+mozprocess==0.22
+mozprofile==0.27
+mozrunner==6.9
+moztest==0.7
+mozversion==1.4
+marionette-client == 2.0.0
+marionette-driver == 1.1.1
+firefox-puppeteer==3.1.0
+
+# Install the firefox media tests package
+./
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/setup.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 setuptools import setup, find_packages
+
+PACKAGE_VERSION = '0.4'
+
+deps = [
+    'marionette-client == 2.0.0',
+    'marionette-driver == 1.1.1',
+    'mozlog == 3.0',
+    'manifestparser == 1.1',
+    'firefox-puppeteer >= 3.1.0, <4.0.0',
+]
+
+setup(name='firefox-media-tests',
+      version=PACKAGE_VERSION,
+      description=('A collection of Mozilla Firefox media playback tests run '
+                   'with Marionette'),
+      classifiers=[
+          'Environment :: Console',
+          'Intended Audience :: Developers',
+          'License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)',
+          'Natural Language :: English',
+          'Operating System :: OS Independent',
+          'Programming Language :: Python',
+          'Topic :: Software Development :: Libraries :: Python Modules',
+      ],
+      keywords='mozilla',
+      author='Mozilla Automation and Tools Team',
+      author_email='tools@lists.mozilla.org',
+      url='https://github.com/mjzffr/firefox-media-tests',
+      license='MPL 2.0',
+      packages=find_packages(),
+      zip_safe=False,
+      install_requires=deps,
+      include_package_data=True,
+      entry_points="""
+        [console_scripts]
+        firefox-media-tests = harness:cli
+    """)