summaryrefslogtreecommitdiffstats
path: root/dom/media/test/external
diff options
context:
space:
mode:
Diffstat (limited to 'dom/media/test/external')
-rw-r--r--dom/media/test/external/MANIFEST.in4
-rw-r--r--dom/media/test/external/README.md5
-rw-r--r--dom/media/test/external/docs/Makefile216
-rw-r--r--dom/media/test/external/docs/conf.py297
-rw-r--r--dom/media/test/external/docs/external_media_harness.rst19
-rw-r--r--dom/media/test/external/docs/external_media_tests.media_tests.video_puppeteer.rst27
-rw-r--r--dom/media/test/external/docs/external_media_tests.media_tests.youtube_puppeteer.rst26
-rw-r--r--dom/media/test/external/docs/external_media_tests.rst26
-rw-r--r--dom/media/test/external/docs/index.rst33
-rw-r--r--dom/media/test/external/docs/make.bat263
-rw-r--r--dom/media/test/external/external_media_harness/__init__.py5
-rw-r--r--dom/media/test/external/external_media_harness/runtests.py103
-rw-r--r--dom/media/test/external/external_media_harness/testcase.py362
-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
-rw-r--r--dom/media/test/external/mach_commands.py74
-rw-r--r--dom/media/test/external/requirements-docs.txt5
-rw-r--r--dom/media/test/external/requirements.txt5
-rw-r--r--dom/media/test/external/setup.py42
52 files changed, 3211 insertions, 0 deletions
diff --git a/dom/media/test/external/MANIFEST.in b/dom/media/test/external/MANIFEST.in
new file mode 100644
index 000000000..fe0b96e77
--- /dev/null
+++ b/dom/media/test/external/MANIFEST.in
@@ -0,0 +1,4 @@
+exclude MANIFEST.in
+include requirements.txt
+recursive-include external_media_harness *
+recursive-include external_media_tests *
diff --git a/dom/media/test/external/README.md b/dom/media/test/external/README.md
new file mode 100644
index 000000000..e806f03bc
--- /dev/null
+++ b/dom/media/test/external/README.md
@@ -0,0 +1,5 @@
+external-media-tests
+===================
+
+Documentation for this library has moved to https://developer.mozilla.org/en-US/docs/Mozilla/QA/external-media-tests.
+
diff --git a/dom/media/test/external/docs/Makefile b/dom/media/test/external/docs/Makefile
new file mode 100644
index 000000000..78c1b7379
--- /dev/null
+++ b/dom/media/test/external/docs/Makefile
@@ -0,0 +1,216 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+PAPER =
+BUILDDIR = _build
+
+# User-friendly check for sphinx-build
+ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
+$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
+endif
+
+# Internal variables.
+PAPEROPT_a4 = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help
+help:
+ @echo "Please use \`make <target>' where <target> is one of"
+ @echo " html to make standalone HTML files"
+ @echo " dirhtml to make HTML files named index.html in directories"
+ @echo " singlehtml to make a single large HTML file"
+ @echo " pickle to make pickle files"
+ @echo " json to make JSON files"
+ @echo " htmlhelp to make HTML files and a HTML help project"
+ @echo " qthelp to make HTML files and a qthelp project"
+ @echo " applehelp to make an Apple Help Book"
+ @echo " devhelp to make HTML files and a Devhelp project"
+ @echo " epub to make an epub"
+ @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+ @echo " latexpdf to make LaTeX files and run them through pdflatex"
+ @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
+ @echo " text to make text files"
+ @echo " man to make manual pages"
+ @echo " texinfo to make Texinfo files"
+ @echo " info to make Texinfo files and run them through makeinfo"
+ @echo " gettext to make PO message catalogs"
+ @echo " changes to make an overview of all changed/added/deprecated items"
+ @echo " xml to make Docutils-native XML files"
+ @echo " pseudoxml to make pseudoxml-XML files for display purposes"
+ @echo " linkcheck to check all external links for integrity"
+ @echo " doctest to run all doctests embedded in the documentation (if enabled)"
+ @echo " coverage to run coverage check of the documentation (if enabled)"
+
+.PHONY: clean
+clean:
+ rm -rf $(BUILDDIR)/*
+
+.PHONY: html
+html:
+ $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+.PHONY: dirhtml
+dirhtml:
+ $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+.PHONY: singlehtml
+singlehtml:
+ $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+ @echo
+ @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+.PHONY: pickle
+pickle:
+ $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+ @echo
+ @echo "Build finished; now you can process the pickle files."
+
+.PHONY: json
+json:
+ $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+ @echo
+ @echo "Build finished; now you can process the JSON files."
+
+.PHONY: htmlhelp
+htmlhelp:
+ $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+ @echo
+ @echo "Build finished; now you can run HTML Help Workshop with the" \
+ ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+.PHONY: qthelp
+qthelp:
+ $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+ @echo
+ @echo "Build finished; now you can run "qcollectiongenerator" with the" \
+ ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+ @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/ExternalMediaTests.qhcp"
+ @echo "To view the help file:"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/ExternalMediaTests.qhc"
+
+.PHONY: applehelp
+applehelp:
+ $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
+ @echo
+ @echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
+ @echo "N.B. You won't be able to view it unless you put it in" \
+ "~/Library/Documentation/Help or install it in your application" \
+ "bundle."
+
+.PHONY: devhelp
+devhelp:
+ $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+ @echo
+ @echo "Build finished."
+ @echo "To view the help file:"
+ @echo "# mkdir -p $$HOME/.local/share/devhelp/ExternalMediaTests"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/ExternalMediaTests"
+ @echo "# devhelp"
+
+.PHONY: epub
+epub:
+ $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+ @echo
+ @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+.PHONY: latex
+latex:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo
+ @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+ @echo "Run \`make' in that directory to run these through (pdf)latex" \
+ "(use \`make latexpdf' here to do that automatically)."
+
+.PHONY: latexpdf
+latexpdf:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through pdflatex..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+.PHONY: latexpdfja
+latexpdfja:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through platex and dvipdfmx..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+.PHONY: text
+text:
+ $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+ @echo
+ @echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+.PHONY: man
+man:
+ $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+ @echo
+ @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+.PHONY: texinfo
+texinfo:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo
+ @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+ @echo "Run \`make' in that directory to run these through makeinfo" \
+ "(use \`make info' here to do that automatically)."
+
+.PHONY: info
+info:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo "Running Texinfo files through makeinfo..."
+ make -C $(BUILDDIR)/texinfo info
+ @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+.PHONY: gettext
+gettext:
+ $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+ @echo
+ @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+.PHONY: changes
+changes:
+ $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+ @echo
+ @echo "The overview file is in $(BUILDDIR)/changes."
+
+.PHONY: linkcheck
+linkcheck:
+ $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+ @echo
+ @echo "Link check complete; look for any errors in the above output " \
+ "or in $(BUILDDIR)/linkcheck/output.txt."
+
+.PHONY: doctest
+doctest:
+ $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+ @echo "Testing of doctests in the sources finished, look at the " \
+ "results in $(BUILDDIR)/doctest/output.txt."
+
+.PHONY: coverage
+coverage:
+ $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
+ @echo "Testing of coverage in the sources finished, look at the " \
+ "results in $(BUILDDIR)/coverage/python.txt."
+
+.PHONY: xml
+xml:
+ $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
+ @echo
+ @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
+
+.PHONY: pseudoxml
+pseudoxml:
+ $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
+ @echo
+ @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
diff --git a/dom/media/test/external/docs/conf.py b/dom/media/test/external/docs/conf.py
new file mode 100644
index 000000000..09b15a3fd
--- /dev/null
+++ b/dom/media/test/external/docs/conf.py
@@ -0,0 +1,297 @@
+# -*- coding: utf-8 -*-
+#
+# External Media Tests documentation build configuration file, created by
+# sphinx-quickstart on Tue Mar 15 15:58:18 2016.
+#
+# This file is execfile()d with the current directory set to its
+# containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import sys
+import os
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#sys.path.insert(0, os.path.abspath('.'))
+
+# -- General configuration ------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+ 'sphinx.ext.autodoc',
+ 'sphinx.ext.viewcode',
+]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix(es) of source filenames.
+# You can specify multiple suffix as a list of string:
+# source_suffix = ['.rst', '.md']
+source_suffix = '.rst'
+
+# The encoding of source files.
+#source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'External Media Tests'
+copyright = u'2015-2016, Mozilla, Inc.'
+author = u'Syd Polk and Maja Frydrychowicz'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = u'0.1'
+# The full version, including alpha/beta/rc tags.
+release = u'0.1'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#
+# This is also used if you do content translation via gettext catalogs.
+# Usually you set "language" from the command line for these cases.
+language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = ['_build']
+
+# The reST default role (used for this markup: `text`) to use for all
+# documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+# If true, keep warnings as "system message" paragraphs in the built documents.
+#keep_warnings = False
+
+# If true, `todo` and `todoList` produce output, else they produce nothing.
+todo_include_todos = False
+
+
+# -- Options for HTML output ----------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+html_theme = 'default'
+
+on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
+
+if not on_rtd:
+ try:
+ import sphinx_rtd_theme
+ html_theme = 'sphinx_rtd_theme'
+ html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
+ except ImportError:
+ pass
+
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further. For a list of options available for each theme, see the
+# documentation.
+#html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+#html_theme_path = []
+
+# The name for this set of Sphinx documents. If None, it defaults to
+# "<project> v<release> documentation".
+#html_title = None
+
+# A shorter title for the navigation bar. Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (relative to this directory) to use as a favicon of
+# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# Add any extra paths that contain custom files (such as robots.txt or
+# .htaccess) here, relative to this directory. These files are copied
+# directly to the root of the documentation.
+#html_extra_path = []
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+#html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_domain_indices = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#html_show_sourcelink = True
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+#html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+#html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it. The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = None
+
+# Language to be used for generating the HTML full-text search index.
+# Sphinx supports the following languages:
+# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
+# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr'
+#html_search_language = 'en'
+
+# A dictionary with options for the search language support, empty by default.
+# Now only 'ja' uses this config value
+#html_search_options = {'type': 'default'}
+
+# The name of a javascript file (relative to the configuration directory) that
+# implements a search results scorer. If empty, the default will be used.
+#html_search_scorer = 'scorer.js'
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'ExternalMediaTestsdoc'
+
+# -- Options for LaTeX output ---------------------------------------------
+
+latex_elements = {
+# The paper size ('letterpaper' or 'a4paper').
+#'papersize': 'letterpaper',
+
+# The font size ('10pt', '11pt' or '12pt').
+#'pointsize': '10pt',
+
+# Additional stuff for the LaTeX preamble.
+#'preamble': '',
+
+# Latex figure (float) alignment
+#'figure_align': 'htbp',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+# author, documentclass [howto, manual, or own class]).
+latex_documents = [
+ (master_doc, 'ExternalMediaTests.tex', u'External Media Tests Documentation',
+ u'Syd Polk and Maja Frydrychowicz', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# If true, show page references after internal links.
+#latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#latex_show_urls = False
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_domain_indices = True
+
+
+# -- Options for manual page output ---------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+ (master_doc, 'externalmediatests', u'External Media Tests Documentation',
+ [author], 1)
+]
+
+# If true, show URL addresses after external links.
+#man_show_urls = False
+
+
+# -- Options for Texinfo output -------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ (master_doc, 'ExternalMediaTests', u'External Media Tests Documentation',
+ author, 'ExternalMediaTests', 'One line description of project.',
+ 'Miscellaneous'),
+]
+
+# Documents to append as an appendix to all manuals.
+#texinfo_appendices = []
+
+# If false, no module index is generated.
+#texinfo_domain_indices = True
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+#texinfo_show_urls = 'footnote'
+
+# If true, do not generate a @detailmenu in the "Top" node's menu.
+#texinfo_no_detailmenu = False
diff --git a/dom/media/test/external/docs/external_media_harness.rst b/dom/media/test/external/docs/external_media_harness.rst
new file mode 100644
index 000000000..b484ce68c
--- /dev/null
+++ b/dom/media/test/external/docs/external_media_harness.rst
@@ -0,0 +1,19 @@
+external_media_harness package
+==============================
+
+Test case classes for use in video tests
+
+external_media_harness.testcase module
+--------------------------------------
+
+.. autoclass:: external_media_harness.testcase.MediaTestCase
+ :members:
+ :show-inheritance:
+
+.. autoclass:: external_media_harness.testcase.NetworkBandwidthTestCase
+ :members:
+ :show-inheritance:
+
+.. autoclass:: external_media_harness.testcase.VideoPlaybackTestsMixin
+ :members:
+ :show-inheritance:
diff --git a/dom/media/test/external/docs/external_media_tests.media_tests.video_puppeteer.rst b/dom/media/test/external/docs/external_media_tests.media_tests.video_puppeteer.rst
new file mode 100644
index 000000000..92b8749c8
--- /dev/null
+++ b/dom/media/test/external/docs/external_media_tests.media_tests.video_puppeteer.rst
@@ -0,0 +1,27 @@
+VideoPuppeteer
+==============
+
+
+video_puppeteer.VideoPuppeteer
+------------------------------
+
+.. autoclass:: external_media_tests.media_utils.video_puppeteer.VideoPuppeteer
+ :members:
+ :show-inheritance:
+
+video_puppeteer.VideoException
+------------------------------
+
+.. autoexception:: external_media_tests.media_utils.video_puppeteer.VideoException
+
+video_puppeteer.playback_started
+--------------------------------
+
+.. autofunction:: external_media_tests.media_utils.video_puppeteer.playback_started
+
+video_puppeteer.playback_done
+-----------------------------
+
+.. autofunction:: external_media_tests.media_utils.video_puppeteer.playback_done
+
+
diff --git a/dom/media/test/external/docs/external_media_tests.media_tests.youtube_puppeteer.rst b/dom/media/test/external/docs/external_media_tests.media_tests.youtube_puppeteer.rst
new file mode 100644
index 000000000..613823e6e
--- /dev/null
+++ b/dom/media/test/external/docs/external_media_tests.media_tests.youtube_puppeteer.rst
@@ -0,0 +1,26 @@
+YoutubePuppeteer
+================
+
+youtube_puppeteer.YouTubePuppeteer
+----------------------------------
+
+.. autoclass:: external_media_tests.media_utils.youtube_puppeteer.YouTubePuppeteer
+ :members:
+ :show-inheritance:
+
+youtube_puppeteer.playback_started
+----------------------------------
+
+.. autofunction:: external_media_tests.media_utils.youtube_puppeteer.playback_started
+
+youtube_puppeteer.playback_done
+-------------------------------
+
+.. autofunction:: external_media_tests.media_utils.youtube_puppeteer.playback_done
+
+youtbue_puppeteer.wait_for_almost_done
+--------------------------------------
+
+.. autofunction:: external_media_tests.media_utils.youtube_puppeteer.wait_for_almost_done
+
+
diff --git a/dom/media/test/external/docs/external_media_tests.rst b/dom/media/test/external/docs/external_media_tests.rst
new file mode 100644
index 000000000..619fc5343
--- /dev/null
+++ b/dom/media/test/external/docs/external_media_tests.rst
@@ -0,0 +1,26 @@
+
+external_media_tests package
+============================
+
+This document highlights the utility classes for tests. In general, the indvidiual tests are not documented here.
+
+Test pacakges
+-------------
+
+.. toctree::
+
+ external_media_tests.media_tests.video_puppeteer
+ external_media_tests.media_tests.youtube_puppeteer
+
+
+external_media_tests.utils.verbose_until
+----------------------------------------
+
+.. autofunction:: external_media_tests.utils.verbose_until
+
+external_media_tests.utils.save_memory_report
+---------------------------------------------
+
+.. autofunction:: external_media_tests.utils.save_memory_report
+
+
diff --git a/dom/media/test/external/docs/index.rst b/dom/media/test/external/docs/index.rst
new file mode 100644
index 000000000..7891346c5
--- /dev/null
+++ b/dom/media/test/external/docs/index.rst
@@ -0,0 +1,33 @@
+.. py:currentmodule:: external_media_tests
+
+External Media Tests
+====================
+
+External Media Tests is a library built on top of `Firefox Puppeter`_ and the `Marionette python client`_. It is designed to test playback of video elements embedded in web pages, independent of vendor. Using this library, you can write tests which play, pause, and stop videos, as well as inspect properties such as currentTime().
+
+.. _Marionette python client: http://marionette-client.readthedocs.org/en/latest
+.. _Firefox Puppeter: http://firefox-puppeteer.readthedocs.org/en/latest/
+
+Installation
+------------
+
+External Media Tests lives in `External Media Tests Source`_. Documentation for installation and usage lives on `External Media Tests`_; this documentation is API documentation for the various pieces of the test library.
+
+.. _External Media Tests Source: https://hg.mozilla.org/dom/media/test/external
+.. _External Media Tests: https://developer.mozilla.org/en-US/docs/Mozilla/QA/external-media-tests
+
+Contents
+--------
+
+.. toctree::
+
+ external_media_harness
+ external_media_tests
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
+
diff --git a/dom/media/test/external/docs/make.bat b/dom/media/test/external/docs/make.bat
new file mode 100644
index 000000000..79bf88203
--- /dev/null
+++ b/dom/media/test/external/docs/make.bat
@@ -0,0 +1,263 @@
+@ECHO OFF
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+ set SPHINXBUILD=sphinx-build
+)
+set BUILDDIR=_build
+set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
+set I18NSPHINXOPTS=%SPHINXOPTS% .
+if NOT "%PAPER%" == "" (
+ set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
+ set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
+)
+
+if "%1" == "" goto help
+
+if "%1" == "help" (
+ :help
+ echo.Please use `make ^<target^>` where ^<target^> is one of
+ echo. html to make standalone HTML files
+ echo. dirhtml to make HTML files named index.html in directories
+ echo. singlehtml to make a single large HTML file
+ echo. pickle to make pickle files
+ echo. json to make JSON files
+ echo. htmlhelp to make HTML files and a HTML help project
+ echo. qthelp to make HTML files and a qthelp project
+ echo. devhelp to make HTML files and a Devhelp project
+ echo. epub to make an epub
+ echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
+ echo. text to make text files
+ echo. man to make manual pages
+ echo. texinfo to make Texinfo files
+ echo. gettext to make PO message catalogs
+ echo. changes to make an overview over all changed/added/deprecated items
+ echo. xml to make Docutils-native XML files
+ echo. pseudoxml to make pseudoxml-XML files for display purposes
+ echo. linkcheck to check all external links for integrity
+ echo. doctest to run all doctests embedded in the documentation if enabled
+ echo. coverage to run coverage check of the documentation if enabled
+ goto end
+)
+
+if "%1" == "clean" (
+ for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
+ del /q /s %BUILDDIR%\*
+ goto end
+)
+
+
+REM Check if sphinx-build is available and fallback to Python version if any
+%SPHINXBUILD% 1>NUL 2>NUL
+if errorlevel 9009 goto sphinx_python
+goto sphinx_ok
+
+:sphinx_python
+
+set SPHINXBUILD=python -m sphinx.__init__
+%SPHINXBUILD% 2> nul
+if errorlevel 9009 (
+ echo.
+ echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+ echo.installed, then set the SPHINXBUILD environment variable to point
+ echo.to the full path of the 'sphinx-build' executable. Alternatively you
+ echo.may add the Sphinx directory to PATH.
+ echo.
+ echo.If you don't have Sphinx installed, grab it from
+ echo.http://sphinx-doc.org/
+ exit /b 1
+)
+
+:sphinx_ok
+
+
+if "%1" == "html" (
+ %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/html.
+ goto end
+)
+
+if "%1" == "dirhtml" (
+ %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
+ goto end
+)
+
+if "%1" == "singlehtml" (
+ %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
+ goto end
+)
+
+if "%1" == "pickle" (
+ %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can process the pickle files.
+ goto end
+)
+
+if "%1" == "json" (
+ %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can process the JSON files.
+ goto end
+)
+
+if "%1" == "htmlhelp" (
+ %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can run HTML Help Workshop with the ^
+.hhp project file in %BUILDDIR%/htmlhelp.
+ goto end
+)
+
+if "%1" == "qthelp" (
+ %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can run "qcollectiongenerator" with the ^
+.qhcp project file in %BUILDDIR%/qthelp, like this:
+ echo.^> qcollectiongenerator %BUILDDIR%\qthelp\ExternalMediaTests.qhcp
+ echo.To view the help file:
+ echo.^> assistant -collectionFile %BUILDDIR%\qthelp\ExternalMediaTests.ghc
+ goto end
+)
+
+if "%1" == "devhelp" (
+ %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished.
+ goto end
+)
+
+if "%1" == "epub" (
+ %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The epub file is in %BUILDDIR%/epub.
+ goto end
+)
+
+if "%1" == "latex" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "latexpdf" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ cd %BUILDDIR%/latex
+ make all-pdf
+ cd %~dp0
+ echo.
+ echo.Build finished; the PDF files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "latexpdfja" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ cd %BUILDDIR%/latex
+ make all-pdf-ja
+ cd %~dp0
+ echo.
+ echo.Build finished; the PDF files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "text" (
+ %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The text files are in %BUILDDIR%/text.
+ goto end
+)
+
+if "%1" == "man" (
+ %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The manual pages are in %BUILDDIR%/man.
+ goto end
+)
+
+if "%1" == "texinfo" (
+ %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
+ goto end
+)
+
+if "%1" == "gettext" (
+ %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
+ goto end
+)
+
+if "%1" == "changes" (
+ %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.The overview file is in %BUILDDIR%/changes.
+ goto end
+)
+
+if "%1" == "linkcheck" (
+ %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Link check complete; look for any errors in the above output ^
+or in %BUILDDIR%/linkcheck/output.txt.
+ goto end
+)
+
+if "%1" == "doctest" (
+ %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Testing of doctests in the sources finished, look at the ^
+results in %BUILDDIR%/doctest/output.txt.
+ goto end
+)
+
+if "%1" == "coverage" (
+ %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Testing of coverage in the sources finished, look at the ^
+results in %BUILDDIR%/coverage/python.txt.
+ goto end
+)
+
+if "%1" == "xml" (
+ %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The XML files are in %BUILDDIR%/xml.
+ goto end
+)
+
+if "%1" == "pseudoxml" (
+ %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
+ goto end
+)
+
+:end
diff --git a/dom/media/test/external/external_media_harness/__init__.py b/dom/media/test/external/external_media_harness/__init__.py
new file mode 100644
index 000000000..906473f0b
--- /dev/null
+++ b/dom/media/test/external/external_media_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
diff --git a/dom/media/test/external/external_media_harness/runtests.py b/dom/media/test/external/external_media_harness/runtests.py
new file mode 100644
index 000000000..08d1a323b
--- /dev/null
+++ b/dom/media/test/external/external_media_harness/runtests.py
@@ -0,0 +1,103 @@
+# 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
+import sys
+
+import mozlog
+
+from manifestparser import read_ini
+from marionette_harness import (
+ BaseMarionetteTestRunner,
+ BaseMarionetteArguments,
+ BrowserMobProxyArguments,
+)
+from marionette_harness.runtests import MarionetteHarness, cli as mn_cli
+
+import external_media_tests
+from testcase import MediaTestCase
+from external_media_tests.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(external_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)
+ if not args.video_urls:
+ raise ValueError('list of video URLs cannot be empty')
+
+ def parse_args_handler(self, args):
+ if not args.tests:
+ args.tests = [external_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 = external_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 parse_args(self, *args, **kwargs):
+ return MarionetteHarness.parse_args(self, {'mach': sys.stdout})
+
+
+def cli():
+ mn_cli(MediaTestRunner, MediaTestArguments, FirefoxMediaHarness)
+
+if __name__ == '__main__':
+ cli()
diff --git a/dom/media/test/external/external_media_harness/testcase.py b/dom/media/test/external/external_media_harness/testcase.py
new file mode 100644
index 000000000..56350ccd9
--- /dev/null
+++ b/dom/media/test/external/external_media_harness/testcase.py
@@ -0,0 +1,362 @@
+# 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
+import os
+
+from marionette_driver import Wait
+from marionette_driver.errors import TimeoutException
+from marionette_harness import (
+ BrowserMobProxyTestCaseMixin,
+ MarionetteTestCase,
+ Marionette,
+ SkipTest,
+)
+
+from firefox_puppeteer import PuppeteerMixin
+from external_media_tests.utils import (timestamp_now, verbose_until)
+from external_media_tests.media_utils.video_puppeteer import (
+ VideoException,
+ VideoPuppeteer
+)
+
+
+class MediaTestCase(PuppeteerMixin, MarionetteTestCase):
+
+ """
+ Necessary methods for MSE playback
+
+ :param video_urls: Urls you are going to play as part of the tests.
+ """
+
+ def __init__(self, *args, **kwargs):
+ self.video_urls = kwargs.pop('video_urls', False)
+ super(MediaTestCase, self).__init__(*args, **kwargs)
+
+ def save_screenshot(self):
+ """
+ Make a screenshot of the window that is currently playing the video
+ element.
+ """
+ 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(Marionette.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 {}'
+ .format(os.path.abspath(path)))
+
+ def log_video_debug_lines(self, video):
+ """
+ Log the debugging information that Firefox provides for video elements.
+ """
+ with self.marionette.using_context(Marionette.CONTEXT_CHROME):
+ debug_lines = video.get_debug_lines()
+ if debug_lines:
+ self.marionette.log('\n'.join(debug_lines))
+
+ def run_playback(self, video):
+ """
+ Play the video all of the way through, or for the requested duration,
+ whichever comes first. Raises if the video stalls for too long.
+
+ :param video: VideoPuppeteer instance to play.
+ """
+ with self.marionette.using_context(Marionette.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, VideoPuppeteer.playback_done)
+ except VideoException as e:
+ raise self.failureException(e)
+
+ def check_playback_starts(self, video):
+ """
+ Check to see if a given video will start. Raises if the video does not
+ start.
+
+ :param video: VideoPuppeteer instance to play.
+ """
+ with self.marionette.using_context(Marionette.CONTEXT_CONTENT):
+ self.logger.info(video.test_url)
+ try:
+ verbose_until(Wait(video, timeout=video.timeout),
+ video, VideoPuppeteer.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):
+ """
+ Test MSE playback while network bandwidth is limited. Uses browsermobproxy
+ (https://bmp.lightbody.net/). Please see
+ https://developer.mozilla.org/en-US/docs/Mozilla/QA/external-media-tests
+ for more information on setting up browsermob_proxy.
+ """
+
+ def __init__(self, *args, **kwargs):
+ super(NetworkBandwidthTestCase, self).__init__(*args, **kwargs)
+ BrowserMobProxyTestCaseMixin.__init__(self, *args, **kwargs)
+ self.proxy = None
+
+ def setUp(self):
+ super(NetworkBandwidthTestCase, self).setUp()
+ BrowserMobProxyTestCaseMixin.setUp(self)
+ self.proxy = self.create_browsermob_proxy()
+
+ def tearDown(self):
+ super(NetworkBandwidthTestCase, self).tearDown()
+ BrowserMobProxyTestCaseMixin.tearDown(self)
+ self.proxy = None
+
+ def run_videos(self, timeout=60):
+ """
+ Run each of the videos in the video list. Raises if something goes
+ wrong in playback.
+ """
+ with self.marionette.using_context(Marionette.CONTEXT_CONTENT):
+ for url in self.video_urls:
+ video = VideoPuppeteer(self.marionette, url, stall_wait_time=60,
+ set_duration=60, timeout=timeout)
+ 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 tests 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):
+ """
+ Test to make sure that playback of the video element starts for each
+ video.
+ """
+ with self.marionette.using_context(Marionette.CONTEXT_CONTENT):
+ for url in self.video_urls:
+ try:
+ video = VideoPuppeteer(self.marionette, url, timeout=60)
+ # Second playback_started check in case video._start_time
+ # is not 0
+ self.check_playback_starts(video)
+ video.pause()
+ except TimeoutException as e:
+ raise self.failureException(e)
+
+ def test_video_playback_partial(self):
+ """
+ Test to make sure that playback of 60 seconds works for each video.
+ """
+ with self.marionette.using_context(Marionette.CONTEXT_CONTENT):
+ for url in self.video_urls:
+ video = VideoPuppeteer(self.marionette, url,
+ stall_wait_time=10,
+ set_duration=60)
+ self.run_playback(video)
+
+
+class NetworkBandwidthTestsMixin(object):
+
+ """
+ Test video urls with various bandwidth settings.
+ """
+
+ def test_playback_limiting_bandwidth_250(self):
+ self.proxy.limits({'downstream_kbps': 250})
+ self.run_videos(timeout=120)
+
+ def test_playback_limiting_bandwidth_500(self):
+ self.proxy.limits({'downstream_kbps': 500})
+ self.run_videos(timeout=120)
+
+ def test_playback_limiting_bandwidth_1000(self):
+ self.proxy.limits({'downstream_kbps': 1000})
+ self.run_videos(timeout=120)
+
+
+reset_adobe_gmp_script = """
+navigator.requestMediaKeySystemAccess('com.adobe.primetime',
+[{initDataTypes: ['cenc']}]).then(
+ function(access) {
+ marionetteScriptFinished('success');
+ },
+ function(ex) {
+ marionetteScriptFinished(ex);
+ }
+);
+"""
+
+
+reset_widevine_gmp_script = """
+navigator.requestMediaKeySystemAccess('com.widevine.alpha',
+[{initDataTypes: ['cenc']}]).then(
+ function(access) {
+ marionetteScriptFinished('success');
+ },
+ function(ex) {
+ marionetteScriptFinished(ex);
+ }
+);
+"""
+
+
+class EMESetupMixin(object):
+
+ """
+ An object that needs to use the Adobe or Widevine GMP system must inherit
+ from this class, and then call check_eme_system() to insure that everything
+ is setup correctly.
+ """
+
+ version_needs_reset = True
+
+ def check_eme_system(self):
+ """
+ Download the most current version of the Adobe and Widevine GMP
+ Plugins. Verify that all MSE and EME prefs are set correctly. Raises
+ if things are not OK.
+ """
+ self.set_eme_prefs()
+ self.reset_GMP_version()
+ assert(self.check_eme_prefs())
+
+ def set_eme_prefs(self):
+ with self.marionette.using_context(Marionette.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.marionette.set_pref('media.gmp.trial-create.enabled', False)
+
+ def reset_GMP_version(self):
+ if EMESetupMixin.version_needs_reset:
+ with self.marionette.using_context(Marionette.CONTEXT_CHROME):
+ if self.marionette.get_pref('media.gmp-eme-adobe.version'):
+ self.marionette.reset_pref('media.gmp-eme-adobe.version')
+ if self.marionette.get_pref('media.gmp-widevinecdm.version'):
+ self.marionette.reset_pref('media.gmp-widevinecdm.version')
+ with self.marionette.using_context(Marionette.CONTEXT_CONTENT):
+ adobe_result = self.marionette.execute_async_script(
+ reset_adobe_gmp_script,
+ script_timeout=60000)
+ widevine_result = self.marionette.execute_async_script(
+ reset_widevine_gmp_script,
+ script_timeout=60000)
+ if not adobe_result == 'success':
+ raise VideoException(
+ 'ERROR: Resetting Adobe GMP failed {}'
+ .format(adobe_result))
+ if not widevine_result == 'success':
+ raise VideoException(
+ 'ERROR: Resetting Widevine GMP failed {}'
+ .format(widevine_result))
+
+ EMESetupMixin.version_needs_reset = False
+
+ def check_and_log_boolean_pref(self, pref_name, expected_value):
+ with self.marionette.using_context(Marionette.CONTEXT_CHROME):
+ pref_value = self.marionette.get_pref(pref_name)
+
+ if pref_value is None:
+ self.logger.info('Pref {} has no value.'.format(pref_name))
+ return False
+ else:
+ self.logger.info('Pref {} = {}'.format(pref_name, pref_value))
+ if pref_value != expected_value:
+ self.logger.info('Pref {} has unexpected value.'
+ .format(pref_name))
+ return False
+
+ return True
+
+ def check_and_log_integer_pref(self, pref_name, minimum_value=0):
+ with self.marionette.using_context(Marionette.CONTEXT_CHROME):
+ pref_value = self.marionette.get_pref(pref_name)
+
+ if pref_value is None:
+ self.logger.info('Pref {} has no value.'.format(pref_name))
+ return False
+ else:
+ self.logger.info('Pref {} = {}'.format(pref_name, pref_value))
+
+ match = re.search('^\d+$', pref_value)
+ if not match:
+ self.logger.info('Pref {} is not an integer'
+ .format(pref_name))
+ return False
+
+ return pref_value >= minimum_value
+
+ def chceck_and_log_version_string_pref(self, pref_name, minimum_value='0'):
+ """
+ Compare a pref made up of integers separated by stops .s, with a
+ version string of the same format. The number of integers in each
+ string does not need to match. The comparison is done by converting
+ each to an integer array and comparing those. Both version strings
+ must be made up of only integers, or this method will raise an
+ unhandled exception of type ValueError when the conversion to int
+ fails.
+ """
+ with self.marionette.using_context(Marionette.CONTEXT_CHROME):
+ pref_value = self.marionette.get_pref(pref_name)
+
+ if pref_value is None:
+ self.logger.info('Pref {} has no value.'.format(pref_name))
+ return False
+ else:
+ self.logger.info('Pref {} = {}'.format(pref_name, pref_value))
+
+ match = re.search('^\d(.\d+)*$', pref_value)
+ if not match:
+ self.logger.info('Pref {} is not a version string'
+ .format(pref_name))
+ return False
+
+ pref_ints = [int(n) for n in pref_value.split('.')]
+ minumum_ints = [int(n) for n in minimum_value.split('.')]
+
+ return pref_ints >= minumum_ints
+
+ def check_eme_prefs(self):
+ with self.marionette.using_context(Marionette.CONTEXT_CHROME):
+ return all([
+ self.check_and_log_boolean_pref(
+ 'media.mediasource.enabled', True),
+ self.check_and_log_boolean_pref(
+ 'media.eme.enabled', True),
+ self.check_and_log_boolean_pref(
+ 'media.mediasource.mp4.enabled', True),
+ self.check_and_log_boolean_pref(
+ 'media.gmp-eme-adobe.enabled', True),
+ self.check_and_log_integer_pref(
+ 'media.gmp-eme-adobe.version', 1),
+ self.check_and_log_boolean_pref(
+ 'media.gmp-widevinecdm.enabled', True),
+ self.chceck_and_log_version_string_pref(
+ 'media.gmp-widevinecdm.version', '1.0.0.0')
+ ])
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)
diff --git a/dom/media/test/external/mach_commands.py b/dom/media/test/external/mach_commands.py
new file mode 100644
index 000000000..e309f3c55
--- /dev/null
+++ b/dom/media/test/external/mach_commands.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 __future__ import absolute_import, unicode_literals
+
+import os
+import sys
+
+from mozbuild.base import (
+ MachCommandBase,
+ MachCommandConditions as conditions,
+)
+
+from mach.decorators import (
+ CommandProvider,
+ Command,
+)
+
+def setup_argument_parser():
+ from external_media_harness.runtests import MediaTestArguments
+ from mozlog.structured import commandline
+ parser = MediaTestArguments()
+ commandline.add_logging_group(parser)
+ return parser
+
+
+def run_external_media_test(tests, testtype=None, topsrcdir=None, **kwargs):
+ from external_media_harness.runtests import (
+ FirefoxMediaHarness,
+ MediaTestArguments,
+ MediaTestRunner,
+ mn_cli,
+ )
+
+ from mozlog.structured import commandline
+
+ from argparse import Namespace
+
+ parser = setup_argument_parser()
+
+ if not tests:
+ tests = [os.path.join(topsrcdir,
+ 'dom/media/test/external/external_media_tests/manifest.ini')]
+
+ args = Namespace(tests=tests)
+
+ for k, v in kwargs.iteritems():
+ setattr(args, k, v)
+
+ parser.verify_usage(args)
+
+ args.logger = commandline.setup_logging("Firefox External Media Tests",
+ args,
+ {"mach": sys.stdout})
+ failed = mn_cli(MediaTestRunner, MediaTestArguments, FirefoxMediaHarness,
+ args=vars(args))
+
+ if failed > 0:
+ return 1
+ else:
+ return 0
+
+
+@CommandProvider
+class MachCommands(MachCommandBase):
+ @Command('external-media-tests', category='testing',
+ description='Run Firefox external media tests.',
+ conditions=[conditions.is_firefox],
+ parser=setup_argument_parser,
+ )
+ def run_external_media_test(self, tests, **kwargs):
+ kwargs['binary'] = kwargs['binary'] or self.get_binary_path('app')
+ return run_external_media_test(tests, topsrcdir=self.topsrcdir, **kwargs)
diff --git a/dom/media/test/external/requirements-docs.txt b/dom/media/test/external/requirements-docs.txt
new file mode 100644
index 000000000..a5109fff1
--- /dev/null
+++ b/dom/media/test/external/requirements-docs.txt
@@ -0,0 +1,5 @@
+sphinx
+sphinx-rtd-theme
+
+-e dom/media/test/external
+
diff --git a/dom/media/test/external/requirements.txt b/dom/media/test/external/requirements.txt
new file mode 100644
index 000000000..07133ca9f
--- /dev/null
+++ b/dom/media/test/external/requirements.txt
@@ -0,0 +1,5 @@
+firefox-puppeteer >= 52.1.0, <53.0.0
+manifestparser==1.1
+marionette-driver==2.2.0
+marionette-harness==4.0.0
+mozlog==3.3
diff --git a/dom/media/test/external/setup.py b/dom/media/test/external/setup.py
new file mode 100644
index 000000000..8520f9c8a
--- /dev/null
+++ b/dom/media/test/external/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/.
+
+import os
+from setuptools import setup, find_packages
+
+PACKAGE_VERSION = '2.1'
+
+THIS_DIR = os.path.dirname(os.path.realpath(__name__))
+
+
+def read(*parts):
+ with open(os.path.join(THIS_DIR, *parts)) as f:
+ return f.read()
+
+setup(name='external-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://hg.mozilla.org/mozilla-central/dom/media/test/external/',
+ license='MPL 2.0',
+ packages=find_packages(),
+ zip_safe=False,
+ install_requires=read('requirements.txt').splitlines(),
+ include_package_data=True,
+ entry_points="""
+ [console_scripts]
+ external-media-tests = external_media_harness:cli
+ """)