author | Tarek Ziadé <tarek@mozilla.com> |
Tue, 02 Apr 2019 15:09:47 +0000 | |
changeset 529496 | a54e5e2802d7e414511662299c69b33fd3bb7612 |
parent 529495 | 3dc567fb205c93430298a314397a3f0d7d7b9470 |
child 529497 | 16044e8bda4b4e0e654b5b8c7c43bd9ad1d8e3da |
push id | 2082 |
push user | ffxbld-merge |
push date | Mon, 01 Jul 2019 08:34:18 +0000 |
treeherder | mozilla-release@2fb19d0466d2 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | whimboo |
bugs | 1527620 |
milestone | 68.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/dom/media/moz.build +++ b/dom/media/moz.build @@ -354,8 +354,12 @@ include('/ipc/chromium/chromium-config.m # defined, which complains about an important MOZ_EXPORT for android::AString if CONFIG['CC_TYPE'] in ('clang', 'gcc'): CXXFLAGS += [ '-Wno-error=attributes', '-Wno-error=shadow', ] FINAL_LIBRARY = 'xul' + +MARIONETTE_DOM_MEDIA_MANIFESTS += [ + 'test/marionette/manifest.ini' +]
new file mode 100644 --- /dev/null +++ b/dom/media/test/marionette/manifest.ini @@ -0,0 +1,4 @@ +[DEFAULT] +run-if = buildapp == 'browser' + +[test_youtube.py]
new file mode 100644 --- /dev/null +++ b/dom/media/test/marionette/test_youtube.py @@ -0,0 +1,28 @@ +# 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 sys +import os + +sys.path.append(os.path.dirname(__file__)) +from yttest.support import VideoStreamTestCase + + +class YoutubeTest(VideoStreamTestCase): + + # bug 1513511 + def test_stream_30_seconds(self): + # XXX use the VP9 video we will settle on. + with self.youtube_video("BZP1rYjoBgI") as page: + res = page.run_test() + self.assertTrue(res is not None, "We did not get back the results") + self.assertLess(res["droppedVideoFrames"], res["totalVideoFrames"] * 0.04) + # extracting in/out from the debugInfo + video_state = res["debugInfo"][7] + video_in = int(video_state.split(" ")[10].split("=")[-1]) + video_out = int(video_state.split(" ")[11].split("=")[-1]) + # what's the ratio ? we want 99%+ + if video_out == video_in: + return + in_out_ratio = float(video_out) / float(video_in) * 100 + self.assertMore(in_out_ratio, 99.0)
new file mode 100644 --- /dev/null +++ b/dom/media/test/marionette/yttest/BZP1rYjoBgI.manifest @@ -0,0 +1,10 @@ +[ + { + "size": 20396656, + "visibility": "public", + "digest": "ccdecb515cadd243608898f38d74c23162fccb9246fee3084834c23d3a57710ed24c7c5dcc9b8bc6f5c3acb5fc0f3be144de08aa14d93e7dbbd372ec6166c138", + "algorithm": "sha512", + "filename": "BZP1rYjoBgI.tar.gz", + "unpack": true + } +]
new file mode 100644 --- /dev/null +++ b/dom/media/test/marionette/yttest/__init__.py @@ -0,0 +1,1 @@ +#
new file mode 100644 --- /dev/null +++ b/dom/media/test/marionette/yttest/debug_info.js @@ -0,0 +1,18 @@ +video.mozRequestDebugInfo().then(debugInfo => { + try { + debugInfo = debugInfo.replace(/\t/g, '').split(/\n/g); + var JSONDebugInfo = "{"; + for(let g =0; g<debugInfo.length-1; g++){ + var pair = debugInfo[g].split(": "); + JSONDebugInfo += '"' + pair[0] + '":"' + pair[1] + '",'; + } + JSONDebugInfo = JSONDebugInfo.slice(0,JSONDebugInfo.length-1); + JSONDebugInfo += "}"; + result["debugInfo"] = JSON.parse(JSONDebugInfo); + } catch (err) { + console.log(`Error '${err.toString()} in JSON.parse(${debugInfo})`); + result["debugInfo"] = debugInfo; + } + result["debugInfo"] = debugInfo; + resolve(result); +});
new file mode 100644 --- /dev/null +++ b/dom/media/test/marionette/yttest/download.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/. +import sys +from pytube import YouTube + + +def download_streams(video_id, output_path="data"): + yt = YouTube("https://youtube.com/watch?v=%s" % video_id) + for stream in yt.streams.all(): + fn = "%s-%s-%s.%s" % (video_id, stream.itag, stream.type, stream.subtype) + stream.download(output_path="data", filename=fn) + print("%s downloaded" % fn) + + +if __name__ == "__main__": + download_streams(sys.argv[-1])
new file mode 100644 --- /dev/null +++ b/dom/media/test/marionette/yttest/duration_test.js @@ -0,0 +1,21 @@ +%(force_hd)s + +const resolve = arguments[arguments.length - 1]; + +// this script is injected by marionette to collect metrics +var video = document.getElementsByTagName("video")[0]; +if (!video) { + return "Can't find the video tag"; +} + +video.addEventListener("timeupdate", () => { + if (video.currentTime >= %(duration)s) { + video.pause(); + %(video_playback_quality)s + %(debug_info)s + } + } +); + +video.play(); +
new file mode 100644 --- /dev/null +++ b/dom/media/test/marionette/yttest/force_hd.js @@ -0,0 +1,73 @@ +// This parts forces the highest definition +// https://addons.mozilla.org/en-US/firefox/addon/youtube-auto-hd-lq/ +// licence: MPL 2.0 +var config = { + "HD": true, + "LQ": false, + "ID": "auto-hd-lq-for-ytb", + "type": function (t) { + config.HD = t === 'hd'; + config.LQ = t === 'lq'; + }, + "quality": function () { + if (config.HD || config.LQ) { + var youtubePlayerListener = function (LQ, HD) { + return function youtubePlayerListener (e) { + if (e === 1) { + var player = document.getElementById('movie_player'); + if (player) { + var levels = player.getAvailableQualityLevels(); + if (levels.length) { + var q = (HD && levels[0]) ? levels[0] : ((LQ && levels[levels.length - 2]) ? levels[levels.length - 2] : null); + if (q) { + player.setPlaybackQuality(q); + player.setPlaybackQualityRange(q, q); + } + } + } + } + } + } + /* */ + var inject = function () { + var action = function () { + var player = document.getElementById('movie_player'); + if (player && player.addEventListener && player.getPlayerState) { + player.addEventListener("onStateChange", "youtubePlayerListener"); + } else window.setTimeout(action, 1000); + }; + /* */ + action(); + }; + var script = document.getElementById(config.ID); + if (!script) { + script = document.createElement("script"); + script.setAttribute("type", "text/javascript"); + script.setAttribute("id", config.ID); + document.documentElement.appendChild(script); + } + /* */ + script.textContent = "var youtubePlayerListener = (" + youtubePlayerListener + ')(' + config.LQ + ',' + config.HD + ');(' + inject + ')();'; + } + } +}; + + if (/^https?:\/\/www\.youtube.com\/watch\?/.test(document.location.href)) config.quality(); + var content = document.getElementById('content'); + if (content) { + var observer = new window.MutationObserver(function (e) { + e.forEach(function (m) { + if (m.addedNodes !== null) { + for (var i = 0; i < m.addedNodes.length; i++) { + if (m.addedNodes[i].id === 'movie_player') { + config.quality(); + return; + } + } + } + }); + }); + /* */ + observer.observe(content, {"childList": true, "subtree": true}); + } +
new file mode 100644 --- /dev/null +++ b/dom/media/test/marionette/yttest/playback.py @@ -0,0 +1,648 @@ +# 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/. +""" +MITM Script used to play back media files when a YT video is played. + +This is a self-contained script that should not import anything else +except modules from the standard library and mitmproxy modules. +""" +import os +import sys +import datetime +import time + + +itags = { + "5": { + "Extension": "flv", + "Resolution": "240p", + "VideoEncoding": "Sorenson H.283", + "AudioEncoding": "mp3", + "Itag": 5, + "AudioBitrate": 64, + }, + "6": { + "Extension": "flv", + "Resolution": "270p", + "VideoEncoding": "Sorenson H.263", + "AudioEncoding": "mp3", + "Itag": 6, + "AudioBitrate": 64, + }, + "13": { + "Extension": "3gp", + "Resolution": "", + "VideoEncoding": "MPEG-4 Visual", + "AudioEncoding": "aac", + "Itag": 13, + "AudioBitrate": 0, + }, + "17": { + "Extension": "3gp", + "Resolution": "144p", + "VideoEncoding": "MPEG-4 Visual", + "AudioEncoding": "aac", + "Itag": 17, + "AudioBitrate": 24, + }, + "18": { + "Extension": "mp4", + "Resolution": "360p", + "VideoEncoding": "H.264", + "AudioEncoding": "aac", + "Itag": 18, + "AudioBitrate": 96, + }, + "22": { + "Extension": "mp4", + "Resolution": "720p", + "VideoEncoding": "H.264", + "AudioEncoding": "aac", + "Itag": 22, + "AudioBitrate": 192, + }, + "34": { + "Extension": "flv", + "Resolution": "480p", + "VideoEncoding": "H.264", + "AudioEncoding": "aac", + "Itag": 34, + "AudioBitrate": 128, + }, + "35": { + "Extension": "flv", + "Resolution": "360p", + "VideoEncoding": "H.264", + "AudioEncoding": "aac", + "Itag": 35, + "AudioBitrate": 128, + }, + "36": { + "Extension": "3gp", + "Resolution": "240p", + "VideoEncoding": "MPEG-4 Visual", + "AudioEncoding": "aac", + "Itag": 36, + "AudioBitrate": 36, + }, + "37": { + "Extension": "mp4", + "Resolution": "1080p", + "VideoEncoding": "H.264", + "AudioEncoding": "aac", + "Itag": 37, + "AudioBitrate": 192, + }, + "38": { + "Extension": "mp4", + "Resolution": "3072p", + "VideoEncoding": "H.264", + "AudioEncoding": "aac", + "Itag": 38, + "AudioBitrate": 192, + }, + "43": { + "Extension": "webm", + "Resolution": "360p", + "VideoEncoding": "VP8", + "AudioEncoding": "vorbis", + "Itag": 43, + "AudioBitrate": 128, + }, + "44": { + "Extension": "webm", + "Resolution": "480p", + "VideoEncoding": "VP8", + "AudioEncoding": "vorbis", + "Itag": 44, + "AudioBitrate": 128, + }, + "45": { + "Extension": "webm", + "Resolution": "720p", + "VideoEncoding": "VP8", + "AudioEncoding": "vorbis", + "Itag": 45, + "AudioBitrate": 192, + }, + "46": { + "Extension": "webm", + "Resolution": "1080p", + "VideoEncoding": "VP8", + "AudioEncoding": "vorbis", + "Itag": 46, + "AudioBitrate": 192, + }, + "82": { + "Extension": "mp4", + "Resolution": "360p", + "VideoEncoding": "H.264", + "Itag": 82, + "AudioBitrate": 96, + }, + "83": { + "Extension": "mp4", + "Resolution": "240p", + "VideoEncoding": "H.264", + "AudioEncoding": "aac", + "Itag": 83, + "AudioBitrate": 96, + }, + "84": { + "Extension": "mp4", + "Resolution": "720p", + "VideoEncoding": "H.264", + "AudioEncoding": "aac", + "Itag": 84, + "AudioBitrate": 192, + }, + "85": { + "Extension": "mp4", + "Resolution": "1080p", + "VideoEncoding": "H.264", + "AudioEncoding": "aac", + "Itag": 85, + "AudioBitrate": 192, + }, + "100": { + "Extension": "webm", + "Resolution": "360p", + "VideoEncoding": "VP8", + "AudioEncoding": "vorbis", + "Itag": 100, + "AudioBitrate": 128, + }, + "101": { + "Extension": "webm", + "Resolution": "360p", + "VideoEncoding": "VP8", + "AudioEncoding": "vorbis", + "Itag": 101, + "AudioBitrate": 192, + }, + "102": { + "Extension": "webm", + "Resolution": "720p", + "VideoEncoding": "VP8", + "AudioEncoding": "vorbis", + "Itag": 102, + "AudioBitrate": 192, + }, + "133": { + "Extension": "mp4", + "Resolution": "240p", + "VideoEncoding": "H.264", + "AudioEncoding": "", + "Itag": 133, + "AudioBitrate": 0, + }, + "134": { + "Extension": "mp4", + "Resolution": "360p", + "VideoEncoding": "H.264", + "AudioEncoding": "", + "Itag": 134, + "AudioBitrate": 0, + }, + "135": { + "Extension": "mp4", + "Resolution": "480p", + "VideoEncoding": "H.264", + "AudioEncoding": "", + "Itag": 135, + "AudioBitrate": 0, + }, + "136": { + "Extension": "mp4", + "Resolution": "720p", + "VideoEncoding": "H.264", + "AudioEncoding": "", + "Itag": 136, + "AudioBitrate": 0, + }, + "137": { + "Extension": "mp4", + "Resolution": "1080p", + "VideoEncoding": "H.264", + "AudioEncoding": "", + "Itag": 137, + "AudioBitrate": 0, + }, + "138": { + "Extension": "mp4", + "Resolution": "2160p", + "VideoEncoding": "H.264", + "AudioEncoding": "", + "Itag": 138, + "AudioBitrate": 0, + }, + "160": { + "Extension": "mp4", + "Resolution": "144p", + "VideoEncoding": "H.264", + "AudioEncoding": "", + "Itag": 160, + "AudioBitrate": 0, + }, + "242": { + "Extension": "webm", + "Resolution": "240p", + "VideoEncoding": "VP9", + "AudioEncoding": "", + "Itag": 242, + "AudioBitrate": 0, + }, + "243": { + "Extension": "webm", + "Resolution": "360p", + "VideoEncoding": "VP9", + "AudioEncoding": "", + "Itag": 243, + "AudioBitrate": 0, + }, + "244": { + "Extension": "webm", + "Resolution": "480p", + "VideoEncoding": "VP9", + "AudioEncoding": "", + "Itag": 244, + "AudioBitrate": 0, + }, + "247": { + "Extension": "webm", + "Resolution": "720p", + "VideoEncoding": "VP9", + "AudioEncoding": "", + "Itag": 247, + "AudioBitrate": 0, + }, + "248": { + "Extension": "webm", + "Resolution": "1080p", + "VideoEncoding": "VP9", + "AudioEncoding": "", + "Itag": 248, + "AudioBitrate": 9, + }, + "264": { + "Extension": "mp4", + "Resolution": "1440p", + "VideoEncoding": "H.264", + "AudioEncoding": "", + "Itag": 264, + "AudioBitrate": 0, + }, + "266": { + "Extension": "mp4", + "Resolution": "2160p", + "VideoEncoding": "H.264", + "AudioEncoding": "", + "Itag": 266, + "AudioBitrate": 0, + }, + "271": { + "Extension": "webm", + "Resolution": "1440p", + "VideoEncoding": "VP9", + "AudioEncoding": "", + "Itag": 271, + "AudioBitrate": 0, + }, + "272": { + "Extension": "webm", + "Resolution": "2160p", + "VideoEncoding": "VP9", + "AudioEncoding": "", + "Itag": 272, + "AudioBitrate": 0, + }, + "278": { + "Extension": "webm", + "Resolution": "144p", + "VideoEncoding": "VP9", + "AudioEncoding": "", + "Itag": 278, + "AudioBitrate": 0, + }, + "298": { + "Extension": "mp4", + "Resolution": "720p", + "VideoEncoding": "H.264", + "AudioEncoding": "", + "Itag": 298, + "AudioBitrate": 0, + }, + "299": { + "Extension": "mp4", + "Resolution": "1080p", + "VideoEncoding": "H.264", + "AudioEncoding": "", + "Itag": 299, + "AudioBitrate": 0, + }, + "302": { + "Extension": "webm", + "Resolution": "720p", + "VideoEncoding": "VP9", + "AudioEncoding": "", + "Itag": 302, + "AudioBitrate": 0, + }, + "303": { + "Extension": "webm", + "Resolution": "1080p", + "VideoEncoding": "VP9", + "AudioEncoding": "", + "Itag": 303, + "AudioBitrate": 0, + }, + "139": { + "Extension": "mp4", + "Resolution": "", + "VideoEncoding": "", + "AudioEncoding": "aac", + "Itag": 139, + "AudioBitrate": 48, + }, + "140": { + "Extension": "mp4", + "Resolution": "", + "VideoEncoding": "", + "AudioEncoding": "aac", + "Itag": 140, + "AudioBitrate": 128, + }, + "141": { + "Extension": "mp4", + "Resolution": "", + "VideoEncoding": "", + "AudioEncoding": "aac", + "Itag": 141, + "AudioBitrate": 256, + }, + "171": { + "Extension": "webm", + "Resolution": "", + "VideoEncoding": "", + "AudioEncoding": "vorbis", + "Itag": 171, + "AudioBitrate": 128, + }, + "172": { + "Extension": "webm", + "Resolution": "", + "VideoEncoding": "", + "AudioEncoding": "vorbis", + "Itag": 172, + "AudioBitrate": 192, + }, + "249": { + "Extension": "webm", + "Resolution": "", + "VideoEncoding": "", + "AudioEncoding": "opus", + "Itag": 249, + "AudioBitrate": 50, + }, + "250": { + "Extension": "webm", + "Resolution": "", + "VideoEncoding": "", + "AudioEncoding": "opus", + "Itag": 250, + "AudioBitrate": 70, + }, + "251": { + "Extension": "webm", + "Resolution": "", + "VideoEncoding": "", + "AudioEncoding": "opus", + "Itag": 251, + "AudioBitrate": 160, + }, + "92": { + "Extension": "ts", + "Resolution": "240p", + "VideoEncoding": "H.264", + "AudioEncoding": "aac", + "Itag": 92, + "AudioBitrate": 48, + }, + "93": { + "Extension": "ts", + "Resolution": "480p", + "VideoEncoding": "H.264", + "AudioEncoding": "aac", + "Itag": 93, + "AudioBitrate": 128, + }, + "94": { + "Extension": "ts", + "Resolution": "720p", + "VideoEncoding": "H.264", + "AudioEncoding": "aac", + "Itag": 94, + "AudioBitrate": 128, + }, + "95": { + "Extension": "ts", + "Resolution": "1080p", + "VideoEncoding": "H.264", + "AudioEncoding": "aac", + "Itag": 95, + "AudioBitrate": 256, + }, + "96": { + "Extension": "ts", + "Resolution": "720p", + "VideoEncoding": "H.264", + "AudioEncoding": "aac", + "Itag": 96, + "AudioBitrate": 256, + }, + "120": { + "Extension": "flv", + "Resolution": "720p", + "VideoEncoding": "H.264", + "AudioEncoding": "aac", + "Itag": 120, + "AudioBitrate": 128, + }, + "127": { + "Extension": "ts", + "Resolution": "", + "VideoEncoding": "", + "AudioEncoding": "aac", + "Itag": 127, + "AudioBitrate": 96, + }, + "128": { + "Extension": "ts", + "Resolution": "", + "VideoEncoding": "", + "AudioEncoding": "aac", + "Itag": 128, + "AudioBitrate": 96, + }, + "132": { + "Extension": "ts", + "Resolution": "240p", + "VideoEncoding": "H.264", + "AudioEncoding": "aac", + "Itag": 132, + "AudioBitrate": 48, + }, + "151": { + "Extension": "ts", + "Resolution": "720p", + "VideoEncoding": "H.264", + "AudioEncoding": "aac", + "Itag": 151, + "AudioBitrate": 24, + }, +} + + +def repr_itag(itag): + itag_info = [" %s: %s" % (k, v) for k, v in get_itag_info(itag).items()] + return "\n".join(itag_info) + + +def get_itag_info(itag): + if itag not in itags: + # unknown itag... + # XXX this could be an issue + return {"Itag": itag, "Error": "Unknown"} + return itags[itag] + + +def log(msg): + print(msg) + + +_HERE = os.path.dirname(__file__) +if "MOZPROXY_DIR" in os.environ: + _DEFAULT_DATA_DIR = os.environ["MOZPROXY_DIR"] +else: + _DEFAULT_DATA_DIR = os.path.join(_HERE, "..", "data") + +_HEADERS = { + b"Last-Modified": b"Mon, 10 Dec 2018 19:39:24 GMT", + b"Content-Type": b"video/webm", + b"Date": b"Wed, 02 Jan 2019 15:14:06 GMT", + b"Expires": b"Wed, 02 Jan 2019 15:14:06 GMT", + b"Cache-Control": b"private, max-age=21292", + b"Accept-Ranges": b"bytes", + b"Content-Length": b"173448", + b"Connection": b"keep-alive", + b"Alt-Svc": b'quic=":443"; ma=2592000; v="44,43,39,35"', + b"Access-Control-Allow-Origin": b"https://www.youtube.com", + b"Access-Control-Allow-Credentials": b"true", + b"Timing-Allow-Origin": b"https://www.youtube.com", + b"Access-Control-Expose-Headers": ( + b"Client-Protocol, Content-Length, " + b"Content-Type, X-Bandwidth-Est, " + b"X-Bandwidth-Est2, X-Bandwidth-Est3, " + b"X-Bandwidth-App-Limited, " + b"X-Bandwidth-Est-App-Limited, " + b"X-Bandwidth-Est-Comp, X-Bandwidth-Avg, " + b"X-Head-Time-Millis, X-Head-Time-Sec, " + b"X-Head-Seqnum, X-Response-Itag, " + b"X-Restrict-Formats-Hint, " + b"X-Sequence-Num, X-Segment-Lmt, " + b"X-Walltime-Ms" + ), + b"X-Restrict-Formats-Hint": b"None", + b"X-Content-Type-Options": b"nosniff", + b"Server": b"gvs 1.0", +} + + +def get_cached_data(request, datadir=_DEFAULT_DATA_DIR): + query_args = dict(request.query) + mime = query_args["mime"] + file_id = query_args["id"] + file_range = query_args["range"] + itag = query_args["itag"] + log("Request File %s - %s" % (file_id, mime)) + log("Requested range %s" % file_range) + log("Requested quality\n%s" % repr_itag(itag)) + frange = file_range.split("-") + range_start, range_end = int(frange[0]), int(frange[1]) + video_id = sys.argv[-1].split(".")[0] + fn = "%s-%s-%s.%s" % (video_id, itag, mime.replace("/", ""), mime.split("/")[-1]) + fn = os.path.join(datadir, fn) + if not os.path.exists(fn): + raise Exception("no file at %s" % fn) + with open(fn, "rb") as f: + data = f.read() + data = data[range_start : range_end + 1] # noqa: E203 + headers = dict(_HEADERS) + headers[b"Content-Type"] = bytes(mime, "utf8") + headers[b"Content-Length"] = bytes(str(len(data)), "utf8") + return headers.items(), data + + +def OK(flow, code=204): + """ Sending back a dummy response. + + 204 is the default in most cases on YT requests. + """ + from mitmproxy import http + + flow.error = None + flow.response = http.HTTPResponse(b"HTTP/1.1", code, b"OK", {}, b"") + + +def request(flow): + # All requests made for stats purposes can be discarded and + # a 204 sent back to the client. + if flow.request.url.startswith("https://www.youtube.com/ptracking"): + OK(flow) + return + if flow.request.url.startswith("https://www.youtube.com/api/stats/playback"): + OK(flow) + return + if flow.request.url.startswith("https://www.youtube.com/api/stats/watchtime"): + OK(flow) + return + # disable a few trackers, sniffers, etc + if "push.services.mozilla.com" in flow.request.url: + OK(flow, code=200) + return + if "gen_204" in flow.request.url: + OK(flow) + return + + # we don't want to post back any data, discarding. + if flow.request.method == "POST": + OK(flow) + return + if "googlevideo.com/videoplayback" in flow.request.url: + from mitmproxy import http + + query_args = dict(flow.request.query) + file_id = query_args["id"] + file_range = query_args["range"] + try: + headers, data = get_cached_data(flow.request) + except Exception: + OK(flow, code=404) + return + headers = list(headers) + flow.error = None + flow.response = http.HTTPResponse(b"HTTP/1.1", 200, b"OK", headers, data) + now = datetime.datetime.now() + then = now - datetime.timedelta(hours=1) + flow.response.timestamp_start = time.mktime(then.timetuple()) + flow.response.refresh() + log("SENT FILE %s IN CACHE - range %s" % (file_id, file_range)) + + +def error(flow): + print("\n\n\n\nERROR %s\n\n\n\n" % flow.error.msg) + + +def tcp_error(flow): + print("\n\n\n\nTCP ERROR %s\n\n\n\n" % flow.error.msg)
new file mode 100644 --- /dev/null +++ b/dom/media/test/marionette/yttest/record.py @@ -0,0 +1,34 @@ +""" +MITM Script used to collect media files when a YT video is played. + +This is a self-contained script that should not import anything else +except modules from the standard library and mitmproxy modules. +""" +import os + + +_HERE = os.path.dirname(__file__) +if "MOZPROXY_DIR" in os.environ: + _DEFAULT_DATA_DIR = os.environ["MOZPROXY_DIR"] +else: + _DEFAULT_DATA_DIR = os.path.join(_HERE, "..", "data") + + +def response(flow): + print(flow.request.url) + if "googlevideo.com/videoplayback" in flow.request.url: + itag = flow.request.query["itag"] + mime = flow.request.query["mime"].replace("/", "-") + query_args = dict(flow.request.query) + file_id = query_args["id"] + file_range = query_args["range"] + print("Writing %s:%s" % (file_id, file_range)) + # changing the host so the MITM recording file + # does not rely on a specific YT server + flow.request.host = "googlevideo.com" + if len(flow.response.content) == 0: + return + path = "%s-%s-%s.%s" % (file_id, itag, file_range, mime) + path = os.path.join(_DEFAULT_DATA_DIR, path) + with open(path, "wb") as f: + f.write(flow.response.content)
new file mode 100644 --- /dev/null +++ b/dom/media/test/marionette/yttest/support.py @@ -0,0 +1,90 @@ +# 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 sys +import os +from contextlib import contextmanager + +from mozproxy import get_playback +import mozinfo +import mozlog +from marionette_harness.marionette_test import MarionetteTestCase +from yttest.ytpage import using_page + + +mozlog.commandline.setup_logging("mozproxy", {}, {"tbpl": sys.stdout}) +here = os.path.dirname(__file__) +playback_script = os.path.join(here, "playback.py") + + +class VideoStreamTestCase(MarionetteTestCase): + def setUp(self): + MarionetteTestCase.setUp(self) + if "MOZ_UPLOAD_DIR" not in os.environ: + os.environ["OBJ_PATH"] = "/tmp/" + self.marionette.set_pref("media.autoplay.default", 1) + + @contextmanager + def using_proxy(self, video_id): + config = {} + config["binary"] = self.marionette.bin + config["app"] = "firefox" + config["platform"] = mozinfo.os + config["processor"] = mozinfo.processor + config["run_local"] = "MOZ_UPLOAD_DIR" not in os.environ + + if "MOZ_UPLOAD_DIR" not in os.environ: + config["obj_path"] = os.environ["OBJ_PATH"] + playback_dir = os.path.join(config["obj_path"], "testing", "mozproxy") + else: + root_dir = os.path.dirname(os.path.dirname(os.environ["MOZ_UPLOAD_DIR"])) + playback_dir = os.path.join(root_dir, "testing", "mozproxy") + + config["host"] = "localhost" + config["playback_tool"] = "mitmproxy" + config["playback_artifacts"] = os.path.join(here, "%s.manifest" % video_id) + + # XXX once Bug 1540622 lands, we can use the version here + # config["playback_version"] = "4.0.4" + # and have playback_binary_manifest default to + # mitmproxy-rel-bin-{playback_version}-{platform}.manifest + # so we don't have to ask amozproxy tool user to provide this: + config[ + "playback_binary_manifest" + ] = "mitmproxy-rel-bin-4.0.4-{platform}.manifest" + + playback_file = os.path.join(playback_dir, "%s.playback" % video_id) + + config["playback_tool_args"] = [ + "--set", + "stream_large_bodies=30", + "--ssl-insecure", + "--server-replay-nopop", + "--set", + "upstream_cert=false", + "-S", + playback_file, + "-s", + playback_script, + video_id, + ] + + proxy = get_playback(config) + if proxy is None: + raise Exception("Could not start Proxy") + proxy.start() + try: + yield proxy + finally: + proxy.stop() + + @contextmanager + def youtube_video(self, video_id, **options): + proxy = options.get("proxy", True) + if proxy: + with self.using_proxy(video_id): + with using_page(video_id, self.marionette, **options) as page: + yield page + else: + with using_page(video_id, self.marionette, **options) as page: + yield page
new file mode 100644 --- /dev/null +++ b/dom/media/test/marionette/yttest/until_end_test.js @@ -0,0 +1,18 @@ +%(force_hd)s + +const resolve = arguments[arguments.length - 1]; + +// this script is injected by marionette to collect metrics +var video = document.getElementsByTagName("video")[0]; +if (!video) { + return "Can't find the video tag"; +} + +video.addEventListener("ended", () => { + video.pause(); + %(video_playback_quality)s + %(debug_info)s + }, {once: true} +); + +video.play();
new file mode 100644 --- /dev/null +++ b/dom/media/test/marionette/yttest/video_playback_quality.js @@ -0,0 +1,7 @@ +var vpq = video.getVideoPlaybackQuality(); +var result = {"currentTime": video.currentTime}; +result["creationTime"] = vpq.creationTime; +result["corruptedVideoFrames"] = vpq.corruptedVideoFrames; +result["droppedVideoFrames"] = vpq.droppedVideoFrames; +result["totalVideoFrames"] = vpq.totalVideoFrames; +result["defaultPlaybackRate"] = video.playbackRate;
new file mode 100644 --- /dev/null +++ b/dom/media/test/marionette/yttest/ytpage.py @@ -0,0 +1,82 @@ +# 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/. +""" +Drives the browser during the playback test. +""" +import contextlib +import os + + +here = os.path.dirname(__file__) +js = os.path.join(here, "until_end_test.js") +with open(js) as f: + UNTIL_END_TEST = f.read() + +js = os.path.join(here, "duration_test.js") +with open(js) as f: + DURATION_TEST = f.read() + +JS_MACROS = {"video_playback_quality": "", "debug_info": "", "force_hd": ""} +for script in JS_MACROS: + js = os.path.join(here, "%s.js" % script) + with open(js) as f: + JS_MACROS[script] = f.read() + + +class YoutubePage: + def __init__(self, video_id, marionette, **options): + self.video_id = video_id + self.marionette = marionette + self.url = "https://www.youtube.com/watch?v=%s" % self.video_id + self.started = False + self.capabilities = { + # We're not using upstream cert sniffing, let's make sure + # the browser accepts mitmproxy ones for all requests + # even if they are incorrect. + "acceptInsecureCerts": True + } + self.options = options + if options.get("proxy", True): + self.capabilities["proxy"] = { + "proxyType": "manual", + "httpProxy": "localhost:8080", + "sslProxy": "localhost:8080", + "noProxy": ["localhost"], + } + + def start_video(self): + self.marionette.start_session(self.capabilities) + self.marionette.timeout.script = 600 + self.marionette.navigate(self.url) + self.started = True + + def run_test(self): + self.start_video() + options = dict(JS_MACROS) + options.update(self.options) + if "duration" in options: + script = DURATION_TEST % options + else: + script = UNTIL_END_TEST % options + self.marionette.set_pref("media.autoplay.default", 0) + return self.execute_async_script(script) + + def execute_async_script(self, script, context=None): + if context is None: + context = self.marionette.CONTEXT_CONTENT + with self.marionette.using_context(context): + return self.marionette.execute_async_script(script, sandbox="system") + + def close(self): + if self.started: + self.marionette.delete_session() + + +@contextlib.contextmanager +def using_page(video_id, marionette, **options): + page = YoutubePage(video_id, marionette, **options) + try: + yield page + finally: + page.close()
--- a/python/mozbuild/mozbuild/action/test_archive.py +++ b/python/mozbuild/mozbuild/action/test_archive.py @@ -133,18 +133,19 @@ ARCHIVE_FILES = { 'harness/marionette_harness/tests', 'puppeteer/firefox/docs', ], }, { 'source': buildconfig.topsrcdir, 'base': '', 'manifests': [ + 'dom/media/test/marionette/manifest.ini', 'testing/marionette/harness/marionette_harness/tests/unit-tests.ini', - 'gfx/tests/marionette/manifest.ini', + 'gfx/tests/marionette/manifest.ini' ], # We also need the manifests and harness_unit tests 'pattern': 'testing/marionette/harness/marionette_harness/tests/**', 'dest': 'marionette/tests', }, { 'source': buildconfig.topobjdir, 'base': '_tests',
--- a/python/mozbuild/mozbuild/frontend/context.py +++ b/python/mozbuild/mozbuild/frontend/context.py @@ -1867,16 +1867,20 @@ VARIABLES = { 'METRO_CHROME_MANIFESTS': (ManifestparserManifestList, list, """List of manifest files defining metro browser chrome tests. """), 'MOCHITEST_CHROME_MANIFESTS': (ManifestparserManifestList, list, """List of manifest files defining mochitest chrome tests. """), + 'MARIONETTE_DOM_MEDIA_MANIFESTS': (ManifestparserManifestList, list, + """List of manifest files defining marionette-media tests. + """), + 'MOCHITEST_MANIFESTS': (ManifestparserManifestList, list, """List of manifest files defining mochitest tests. """), 'REFTEST_MANIFESTS': (ReftestManifestList, list, """List of manifest files defining reftests. These are commonly named reftest.list.
--- a/taskcluster/ci/test/marionette.yml +++ b/taskcluster/ci/test/marionette.yml @@ -42,8 +42,18 @@ marionette-gpu: instance-size: default mozharness: extra-options: - --test-manifest gpu-unit-tests.ini virtualization: by-test-platform: windows10-64.*: virtual-with-gpu default: virtual + +marionette-media: + description: "Marionette DOM media test run" + treeherder-symbol: MnM + max-run-time: 5400 + instance-size: default + tier: 2 + mozharness: + extra-options: + - --test-manifest=stream-tests.ini
--- a/taskcluster/ci/test/test-platforms.yml +++ b/taskcluster/ci/test/test-platforms.yml @@ -31,29 +31,31 @@ linux32-devedition/opt: - linux32-opt-tests linux64/debug: build-platform: linux64/debug test-sets: - common-tests - web-platform-tests - mochitest-headless + - marionette-media-tests linux64/opt: build-platform: linux64/opt test-sets: - common-tests - web-platform-tests - opt-only-tests - desktop-screenshot-capture - talos - awsy - mochitest-headless - linux-talos-flex - raptor-firefox - raptor-fetch-firefox + - marionette-media-tests linux64-shippable/opt: build-platform: linux64-shippable/opt test-sets: - common-tests - web-platform-tests - opt-only-tests - desktop-screenshot-capture - talos @@ -182,28 +184,30 @@ windows10-64-ccov/debug: windows10-64/debug: build-platform: win64/debug test-sets: - marionette-gpu-tests - windows-tests - web-platform-tests - mochitest-headless + - marionette-media-tests windows10-64/opt: build-platform: win64/opt test-sets: - awsy - desktop-screenshot-capture - windows-talos - marionette-gpu-tests - windows-tests - web-platform-tests - mochitest-headless - raptor-firefox + - marionette-media-tests # windows10-aarch64/opt: # build-platform: win64-aarch64/opt # test-sets: # - awsy # - desktop-screenshot-capture # - windows-talos # - marionette-gpu-tests @@ -273,28 +277,29 @@ windows10-64-shippable-qr/opt: ## # MacOS X platforms (matching /macosx.*/) macosx64/debug: build-platform: macosx64/debug test-sets: - macosx64-tests + - marionette-media-tests macosx64-shippable/opt: build-platform: macosx64-shippable/opt test-sets: - macosx64-talos - macosx64-tests - desktop-screenshot-capture - awsy - raptor-chrome - raptor-firefox - raptor-profiling - + - marionette-media-tests macosx64-devedition/opt: build-platform: macosx64-devedition-nightly/opt test-sets: - macosx64-tests macosx64-shippable-qr/opt: build-platform: macosx64-shippable/opt test-sets:
--- a/taskcluster/ci/test/test-sets.yml +++ b/taskcluster/ci/test/test-sets.yml @@ -269,16 +269,19 @@ windows-talos: - talos-tp5o - talos-xperf - talos-tabswitch # - talos-h1 Bug 1487031 - Disabled for not finding actionable regressions marionette-gpu-tests: - marionette-gpu +marionette-media-tests: + - marionette-media + macosx64-tests: - cppunit - crashtest - firefox-ui-functional-local - firefox-ui-functional-remote - gtest - jittest - jsreftest
new file mode 100644 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/stream-tests.ini @@ -0,0 +1,2 @@ +# stream tests +[include:../../../../../dom/media/test/marionette/manifest.ini]
--- a/testing/mozbase/mozproxy/mozproxy/backends/mitm.py +++ b/testing/mozbase/mozproxy/mozproxy/backends/mitm.py @@ -74,17 +74,18 @@ POLICIES_CONTENT_OFF = """{ class Mitmproxy(Playback): def __init__(self, config): self.config = config self.mitmproxy_proc = None self.mitmdump_path = None self.browser_path = config.get("binary") self.policies_dir = None self.ignore_mitmdump_exit_failure = config.get( - "ignore_mitmdump_exit_failure", False) + "ignore_mitmdump_exit_failure", False + ) # mozproxy_dir is where we will download all mitmproxy required files # when running locally it comes from obj_path via mozharness/mach if self.config.get("obj_path") is not None: self.mozproxy_dir = self.config.get("obj_path") else: # in production it is ../tasks/task_N/build/, in production that dir # is not available as an envvar, however MOZ_UPLOAD_DIR is set as @@ -144,18 +145,23 @@ class Mitmproxy(Playback): if "playback_artifacts" in self.config: artifacts = self.config["playback_artifacts"].split(",") for artifact in artifacts: artifact = artifact.strip() if not artifact: continue artifact_name = artifact.split("/")[-1] - dest = os.path.join(self.mozproxy_dir, artifact_name) - download_file_from_url(artifact, dest, extract=True) + if artifact_name.endswith(".manifest"): + tooltool_download( + artifact, self.config["run_local"], self.mozproxy_dir + ) + else: + dest = os.path.join(self.mozproxy_dir, artifact_name) + download_file_from_url(artifact, dest, extract=True) def stop(self): self.stop_mitmproxy_playback() def start_mitmproxy_playback(self, mitmdump_path, browser_path): """Startup mitmproxy and replay the specified flow file""" if self.mitmproxy_proc is not None: raise Exception("Proxy already started.") @@ -169,20 +175,19 @@ class Mitmproxy(Playback): if "playback_tool_args" in self.config: command.extend(self.config["playback_tool_args"]) LOG.info("Starting mitmproxy playback using env path: %s" % env["PATH"]) LOG.info("Starting mitmproxy playback using command: %s" % " ".join(command)) # to turn off mitmproxy log output, use these params for Popen: # Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env) - self.mitmproxy_proc = ProcessHandler(command, - logfile=os.path.join(self.upload_dir, - "mitmproxy.log"), - env=env) + self.mitmproxy_proc = ProcessHandler( + command, logfile=os.path.join(self.upload_dir, "mitmproxy.log"), env=env + ) self.mitmproxy_proc.run() end_time = time.time() + MITMDUMP_COMMAND_TIMEOUT ready = False while time.time() < end_time: ready = self.check_proxy() if ready: LOG.info( "Mitmproxy playback successfully started as pid %d" @@ -343,17 +348,17 @@ class MitmproxyDesktop(Mitmproxy): class MitmproxyAndroid(Mitmproxy): def __init__(self, config, android_device): Mitmproxy.__init__(self, config) self.android_device = android_device @property def certutil_sleep_seconds(self): """Time to sleep, in seconds, after issuing a `certutil` command.""" - return 10 if not self.config['run_local'] else 1 + return 10 if not self.config["run_local"] else 1 def setup(self): """For geckoview we need to install the generated mitmproxy CA cert""" if self.config["app"] in ["geckoview", "refbrow", "fenix"]: # install the generated CA certificate into android geckoview self.install_mitmproxy_cert(self.browser_path) def install_mitmproxy_cert(self, browser_path): @@ -367,24 +372,28 @@ class MitmproxyAndroid(Mitmproxy): Then, both locally and in production: 1. Create an NSS certificate database in the geckoview browser profile dir, only if it doesn't already exist. Use this certutil command: `certutil -N -d sql:<path to profile> --empty-password` 2. Import the mitmproxy certificate into the database, i.e.: `certutil -A -d sql:<path to profile> -n "some nickname" -t TC,, -a -i <path to CA.pem>` """ - if self.config['run_local']: + if self.config["run_local"]: # when running locally, it is found in the Firefox desktop build (..obj../dist/bin) - self.certutil = os.path.join(os.environ['MOZ_HOST_BIN'], 'certutil') - if not (os.path.isfile(self.certutil) and os.access(self.certutil, os.X_OK)): - LOG.critical("Abort: unable to execute certutil: {}".format(self.certutil)) + self.certutil = os.path.join(os.environ["MOZ_HOST_BIN"], "certutil") + if not ( + os.path.isfile(self.certutil) and os.access(self.certutil, os.X_OK) + ): + LOG.critical( + "Abort: unable to execute certutil: {}".format(self.certutil) + ) raise - self.certutil = os.environ['MOZ_HOST_BIN'] - os.environ['LD_LIBRARY_PATH'] = self.certutil + self.certutil = os.environ["MOZ_HOST_BIN"] + os.environ["LD_LIBRARY_PATH"] = self.certutil else: # must download certutil inside hostutils via tooltool; use this manifest: # mozilla-central/testing/config/tooltool-manifests/linux64/hostutils.manifest # after it will be found here inside the worker/bitbar container: # /builds/worker/workspace/build/hostutils/host-utils-66.0a1.en-US.linux-x86_64 LOG.info("downloading certutil binary (hostutils)") # get path to the hostutils tooltool manifest; was set earlier in
--- a/testing/mozbase/mozproxy/mozproxy/utils.py +++ b/testing/mozbase/mozproxy/mozproxy/utils.py @@ -25,20 +25,25 @@ except ImportError: from mozlog import get_proxy_logger from mozprocess import ProcessHandler from mozproxy import mozharness_dir LOG = get_proxy_logger(component="mozproxy") -external_tools_path = os.environ.get("EXTERNALTOOLSPATH", None) -if external_tools_path is not None: - # running in production via mozharness - TOOLTOOL_PATH = os.path.join(external_tools_path, "tooltool.py") +if "MOZ_UPLOAD_DIR" in os.environ: + TOOLTOOL_PATH = os.path.join( + os.environ["MOZ_UPLOAD_DIR"], + "..", + "..", + "mozharness", + "external_tools", + "tooltool.py", + ) else: # running locally via mach TOOLTOOL_PATH = os.path.join(mozharness_dir, "external_tools", "tooltool.py") def transform_platform(str_to_transform, config_platform, config_processor=None): """Transform platform name i.e. 'mitmproxy-rel-bin-{platform}.manifest' @@ -75,20 +80,25 @@ def tooltool_download(manifest, run_loca if run_local: command = [sys.executable, TOOLTOOL_PATH, "fetch", "-o", "-m", manifest] else: # Attempt to determine the tooltool cache path: # - TOOLTOOLCACHE is used by Raptor tests # - TOOLTOOL_CACHE is automatically set for taskcluster jobs # - fallback to a hardcoded path - _cache = next(x for x in ( - os.environ.get("TOOLTOOLCACHE"), - os.environ.get("TOOLTOOL_CACHE"), - "/builds/tooltool_cache") if x is not None) + _cache = next( + x + for x in ( + os.environ.get("TOOLTOOLCACHE"), + os.environ.get("TOOLTOOL_CACHE"), + "/builds/tooltool_cache", + ) + if x is not None + ) command = [ sys.executable, TOOLTOOL_PATH, "fetch", "-o", "-m", manifest,