diff --git a/Makefile.in b/Makefile.in index 4c8baebb9f5a..04cfaf39ff5b 100644 --- a/Makefile.in +++ b/Makefile.in @@ -319,7 +319,7 @@ ifdef MOZ_CRASHREPORTER grep 'sym' $(SYMBOL_INDEX_NAME) > $(SYMBOL_INDEX_NAME).tmp && \ mv $(SYMBOL_INDEX_NAME).tmp $(SYMBOL_INDEX_NAME) cd $(DIST)/crashreporter-symbols && \ - zip -r5D '../$(PKG_PATH)$(SYMBOL_ARCHIVE_BASENAME).zip' . -i '*.sym' -i '*.txt' -x '*test*' -x '*Test*' + zip -r5D '../$(PKG_PATH)$(SYMBOL_ARCHIVE_BASENAME).zip' . -i '*.sym' -i '*.txt' endif # MOZ_CRASHREPORTER uploadsymbols: diff --git a/browser/base/content/content.js b/browser/base/content/content.js index 6e5393ae173e..a91cc6c05519 100644 --- a/browser/base/content/content.js +++ b/browser/base/content/content.js @@ -500,7 +500,8 @@ var ClickEventHandler = { ctrlKey: event.ctrlKey, metaKey: event.metaKey, altKey: event.altKey, href: null, title: null, bookmark: false, referrerPolicy: referrerPolicy, - originAttributes: principal ? principal.originAttributes : {} }; + originAttributes: principal ? principal.originAttributes : {}, + isContentWindowPrivate: PrivateBrowsingUtils.isContentWindowPrivate(ownerDoc.defaultView)}; if (href) { try { diff --git a/browser/base/content/test/general/browser.ini b/browser/base/content/test/general/browser.ini index 95ed7abe2e20..13dbbc4350e1 100644 --- a/browser/base/content/test/general/browser.ini +++ b/browser/base/content/test/general/browser.ini @@ -284,6 +284,7 @@ subsuite = clipboard skip-if = true # Disabled due to the clipboard not supporting real file types yet (bug 1288773) [browser_contentAreaClick.js] skip-if = e10s # Clicks in content don't go through contentAreaClick with e10s. +[browser_contentAltClick.js] [browser_contextmenu.js] subsuite = clipboard tags = fullscreen diff --git a/browser/base/content/test/general/browser_contentAltClick.js b/browser/base/content/test/general/browser_contentAltClick.js new file mode 100644 index 000000000000..af4941b76986 --- /dev/null +++ b/browser/base/content/test/general/browser_contentAltClick.js @@ -0,0 +1,107 @@ +/* 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/. */ + +/** + * Test for Bug 1109146. + * The tests opens a new tab and alt + clicks to download files + * and confirms those files are on the download list. + * + * The difference between this and the test "browser_contentAreaClick.js" is that + * the code path in e10s uses ContentClick.jsm instead of browser.js::contentAreaClick() util. + */ +"use strict"; + +XPCOMUtils.defineLazyModuleGetter(this, "Downloads", + "resource://gre/modules/Downloads.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils", + "resource://testing-common/PlacesTestUtils.jsm"); + +function setup(){ + gPrefService.setBoolPref("browser.altClickSave", true); + + let testPage = + 'data:text/html,' + + '

Common link

' + + '

MathML XLink

' + + '

SVG XLink

'; + + return BrowserTestUtils.openNewForegroundTab(gBrowser, testPage); +} + +function* clean_up() { + // Remove downloads. + let downloadList = yield Downloads.getList(Downloads.ALL); + let downloads = yield downloadList.getAll(); + for (let download of downloads) { + yield downloadList.remove(download); + yield download.finalize(true); + } + // Remove download history. + yield PlacesTestUtils.clearHistory(); + + gPrefService.clearUserPref("browser.altClickSave"); + yield BrowserTestUtils.removeTab(gBrowser.selectedTab); +} + +add_task(function* test_alt_click() +{ + yield setup(); + + let downloadList = yield Downloads.getList(Downloads.ALL); + let downloads = []; + let downloadView; + // When 1 download has been attempted then resolve the promise. + let finishedAllDownloads = new Promise( (resolve)=> { + downloadView = { + onDownloadAdded: function (aDownload) { + downloads.push(aDownload); + resolve(); + }, + }; + }); + yield downloadList.addView(downloadView); + yield BrowserTestUtils.synthesizeMouseAtCenter("#commonlink", {altKey: true}, gBrowser.selectedBrowser); + + // Wait for all downloads to be added to the download list. + yield finishedAllDownloads; + yield downloadList.removeView(downloadView); + + is(downloads.length, 1, "1 downloads"); + is(downloads[0].source.url, "http://mochi.test/moz/", "Downloaded #commonlink element"); + + yield* clean_up(); +}); + +add_task(function* test_alt_click_on_xlinks() +{ + yield setup(); + + let downloadList = yield Downloads.getList(Downloads.ALL); + let downloads = []; + let downloadView; + // When all 2 downloads have been attempted then resolve the promise. + let finishedAllDownloads = new Promise( (resolve)=> { + downloadView = { + onDownloadAdded: function (aDownload) { + downloads.push(aDownload); + if (downloads.length == 2) { + resolve(); + } + }, + }; + }); + yield downloadList.addView(downloadView); + yield BrowserTestUtils.synthesizeMouseAtCenter("#mathxlink", {altKey: true}, gBrowser.selectedBrowser); + yield BrowserTestUtils.synthesizeMouseAtCenter("#svgxlink", {altKey: true}, gBrowser.selectedBrowser); + + // Wait for all downloads to be added to the download list. + yield finishedAllDownloads; + yield downloadList.removeView(downloadView); + + is(downloads.length, 2, "2 downloads"); + is(downloads[0].source.url, "http://mochi.test/moz/", "Downloaded #mathxlink element"); + is(downloads[1].source.url, "http://mochi.test/moz/", "Downloaded #svgxlink element"); + + yield* clean_up(); +}); \ No newline at end of file diff --git a/browser/base/content/utilityOverlay.js b/browser/base/content/utilityOverlay.js index 2bac7d00b3c3..b79bf2afc7e0 100644 --- a/browser/base/content/utilityOverlay.js +++ b/browser/base/content/utilityOverlay.js @@ -224,13 +224,20 @@ function openLinkIn(url, where, params) { var aIndicateErrorPageLoad = params.indicateErrorPageLoad; if (where == "save") { - if (!aInitiatingDoc) { - Components.utils.reportError("openUILink/openLinkIn was called with " + - "where == 'save' but without initiatingDoc. See bug 814264."); - return; - } // TODO(1073187): propagate referrerPolicy. - saveURL(url, null, null, true, null, aNoReferrer ? null : aReferrerURI, aInitiatingDoc); + + // ContentClick.jsm passes isContentWindowPrivate for saveURL instead of passing a CPOW initiatingDoc + if ("isContentWindowPrivate" in params) { + saveURL(url, null, null, true, true, aNoReferrer ? null : aReferrerURI, null, params.isContentWindowPrivate); + } + else { + if (!aInitiatingDoc) { + Components.utils.reportError("openUILink/openLinkIn was called with " + + "where == 'save' but without initiatingDoc. See bug 814264."); + return; + } + saveURL(url, null, null, true, true, aNoReferrer ? null : aReferrerURI, aInitiatingDoc); + } return; } diff --git a/browser/components/downloads/DownloadsCommon.jsm b/browser/components/downloads/DownloadsCommon.jsm index 5ebb6ad1f9eb..6800bfe547fb 100644 --- a/browser/components/downloads/DownloadsCommon.jsm +++ b/browser/components/downloads/DownloadsCommon.jsm @@ -508,20 +508,32 @@ this.DownloadsCommon = { // or the file doesn't exist), try using the parent if we have it. let parent = aFile.parent; if (parent) { - try { - // Open the parent directory to show where the file should be. - parent.launch(); - } catch (ex) { - // If launch also fails (probably because it's not implemented), let - // the OS handler try to open the parent. - Cc["@mozilla.org/uriloader/external-protocol-service;1"] - .getService(Ci.nsIExternalProtocolService) - .loadUrl(NetUtil.newURI(parent)); - } + this.showDirectory(parent); } } }, + /** + * Show the specified folder in the system file manager. + * + * @param aDirectory + * a directory to be opened with system file manager. + */ + showDirectory(aDirectory) { + if (!(aDirectory instanceof Ci.nsIFile)) { + throw new Error("aDirectory must be a nsIFile object"); + } + try { + aDirectory.launch(); + } catch (ex) { + // If launch fails (probably because it's not implemented), let + // the OS handler try to open the directory. + Cc["@mozilla.org/uriloader/external-protocol-service;1"] + .getService(Ci.nsIExternalProtocolService) + .loadUrl(NetUtil.newURI(aDirectory)); + } + }, + /** * Displays an alert message box which asks the user if they want to * unblock the downloaded file or not. diff --git a/browser/components/downloads/content/downloads.css b/browser/components/downloads/content/downloads.css index 7077618ef24d..c31c66a0ed4b 100644 --- a/browser/components/downloads/content/downloads.css +++ b/browser/components/downloads/content/downloads.css @@ -19,7 +19,7 @@ richlistitem[type="download"].download-state[state="1"]:not([exists]) .downloadS #downloadsSummary:not([inprogress]) > vbox > #downloadsSummaryProgress, #downloadsSummary:not([inprogress]) > vbox > #downloadsSummaryDetails, -#downloadsFooter[showingsummary] > #downloadsHistory, +#downloadsFooter[showingsummary] > #downloadsFooterButtons, #downloadsFooter:not([showingsummary]) > #downloadsSummary { display: none; } diff --git a/browser/components/downloads/content/downloads.js b/browser/components/downloads/content/downloads.js index 5abb8ffb3066..335fac9be856 100644 --- a/browser/components/downloads/content/downloads.js +++ b/browser/components/downloads/content/downloads.js @@ -367,6 +367,23 @@ const DownloadsPanel = { this._state = this.kStateHidden; }, + onFooterPopupShowing(aEvent) { + let itemClearList = document.getElementById("downloadsDropdownItemClearList"); + if (DownloadsCommon.getData(window).canRemoveFinished) { + itemClearList.removeAttribute("hidden"); + } else { + itemClearList.setAttribute("hidden", "true"); + } + + document.getElementById("downloadsFooterButtonsSplitter").classList + .add("downloadsDropmarkerSplitterExtend"); + }, + + onFooterPopupHidden(aEvent) { + document.getElementById("downloadsFooterButtonsSplitter").classList + .remove("downloadsDropmarkerSplitterExtend"); + }, + ////////////////////////////////////////////////////////////////////////////// //// Related operations @@ -382,6 +399,13 @@ const DownloadsPanel = { BrowserDownloadsUI(); }, + openDownloadsFolder() { + Downloads.getPreferredDownloadsDirectory().then(downloadsPath => { + DownloadsCommon.showDirectory(new FileUtils.File(downloadsPath)); + }).catch(Cu.reportError); + this.hidePanel(); + }, + ////////////////////////////////////////////////////////////////////////////// //// Internal functions @@ -1188,6 +1212,9 @@ const DownloadsViewController = { //// nsIController supportsCommand(aCommand) { + if (aCommand === "downloadsCmd_clearList") { + return true; + } // Firstly, determine if this is a command that we can handle. if (!DownloadsViewUI.isCommandName(aCommand)) { return false; diff --git a/browser/components/downloads/content/downloadsOverlay.xul b/browser/components/downloads/content/downloadsOverlay.xul index 96569569780f..e32142e0bdf0 100644 --- a/browser/components/downloads/content/downloadsOverlay.xul +++ b/browser/components/downloads/content/downloadsOverlay.xul @@ -104,8 +104,8 @@ + label="&cmd.clearList2.label;" + accesskey="&cmd.clearList2.accesskey;"/> - + diff --git a/browser/components/downloads/test/browser/browser.ini b/browser/components/downloads/test/browser/browser.ini index 4e40c7219aa1..08d37cb8f2d4 100644 --- a/browser/components/downloads/test/browser/browser.ini +++ b/browser/components/downloads/test/browser/browser.ini @@ -10,3 +10,4 @@ skip-if = os == "linux" # Bug 952422 [browser_confirm_unblock_download.js] [browser_iframe_gone_mid_download.js] [browser_downloads_panel_block.js] +[browser_downloads_panel_footer.js] diff --git a/browser/components/downloads/test/browser/browser_downloads_panel_footer.js b/browser/components/downloads/test/browser/browser_downloads_panel_footer.js new file mode 100644 index 000000000000..421692177714 --- /dev/null +++ b/browser/components/downloads/test/browser/browser_downloads_panel_footer.js @@ -0,0 +1,94 @@ +"use strict"; + +function *task_openDownloadsSubPanel() { + let downloadSubPanel = document.getElementById("downloadSubPanel"); + let popupShownPromise = BrowserTestUtils.waitForEvent(downloadSubPanel, "popupshown"); + + let downloadsDropmarker = document.getElementById("downloadsFooterDropmarker"); + EventUtils.synthesizeMouseAtCenter(downloadsDropmarker, {}, window); + + yield popupShownPromise; +} + +add_task(function* test_openDownloadsFolder() { + yield task_openPanel(); + + yield task_openDownloadsSubPanel(); + + yield new Promise(resolve => { + sinon.stub(DownloadsCommon, "showDirectory", file => { + resolve(Downloads.getPreferredDownloadsDirectory().then(downloadsPath => { + is(file.path, downloadsPath, "Check the download folder path."); + })); + }); + + let itemOpenDownloadsFolder = + document.getElementById("downloadsDropdownItemOpenDownloadsFolder"); + EventUtils.synthesizeMouseAtCenter(itemOpenDownloadsFolder, {}, window); + }); + + yield task_resetState(); +}); + +add_task(function* test_clearList() { + const kTestCases = [{ + downloads: [ + { state: nsIDM.DOWNLOAD_NOTSTARTED }, + { state: nsIDM.DOWNLOAD_FINISHED }, + { state: nsIDM.DOWNLOAD_FAILED }, + { state: nsIDM.DOWNLOAD_CANCELED }, + ], + expectClearListShown: true, + expectedItemNumber: 0, + },{ + downloads: [ + { state: nsIDM.DOWNLOAD_NOTSTARTED }, + { state: nsIDM.DOWNLOAD_FINISHED }, + { state: nsIDM.DOWNLOAD_FAILED }, + { state: nsIDM.DOWNLOAD_PAUSED }, + { state: nsIDM.DOWNLOAD_CANCELED }, + ], + expectClearListShown: true, + expectedItemNumber: 1, + },{ + downloads: [ + { state: nsIDM.DOWNLOAD_PAUSED }, + ], + expectClearListShown: false, + expectedItemNumber: 1, + }]; + + for (let testCase of kTestCases) { + yield verify_clearList(testCase); + } +}); + +function *verify_clearList(testCase) { + let downloads = testCase.downloads; + yield task_addDownloads(downloads); + + yield task_openPanel(); + is(DownloadsView._downloads.length, downloads.length, + "Expect the number of download items"); + + yield task_openDownloadsSubPanel(); + + let itemClearList = document.getElementById("downloadsDropdownItemClearList"); + let itemNumberPromise = BrowserTestUtils.waitForCondition(() => { + return DownloadsView._downloads.length === testCase.expectedItemNumber; + }); + if (testCase.expectClearListShown) { + isnot("true", itemClearList.getAttribute("hidden"), + "Should show Clear Preview Panel button"); + EventUtils.synthesizeMouseAtCenter(itemClearList, {}, window); + } else { + is("true", itemClearList.getAttribute("hidden"), + "Should not show Clear Preview Panel button"); + } + + yield itemNumberPromise; + is(DownloadsView._downloads.length, testCase.expectedItemNumber, + "Download items remained."); + + yield task_resetState(); +} diff --git a/browser/components/downloads/test/browser/head.js b/browser/components/downloads/test/browser/head.js index a924ac766bc3..a9f7f744702d 100644 --- a/browser/components/downloads/test/browser/head.js +++ b/browser/components/downloads/test/browser/head.js @@ -24,8 +24,17 @@ const nsIDM = Ci.nsIDownloadManager; var gTestTargetFile = FileUtils.getFile("TmpD", ["dm-ui-test.file"]); gTestTargetFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE); + +// Load mocking/stubbing library, sinon +// docs: http://sinonjs.org/docs/ +Services.scriptloader.loadSubScript("resource://testing-common/sinon-1.16.1.js"); + registerCleanupFunction(function () { gTestTargetFile.remove(false); + + delete window.sinon; + delete window.setImmediate; + delete window.clearImmediate; }); //////////////////////////////////////////////////////////////////////////////// diff --git a/browser/components/extensions/test/browser/browser_ext_pageAction_context.js b/browser/components/extensions/test/browser/browser_ext_pageAction_context.js index d43874e4af5b..145f352cd2e2 100644 --- a/browser/components/extensions/test/browser/browser_ext_pageAction_context.js +++ b/browser/components/extensions/test/browser/browser_ext_pageAction_context.js @@ -135,12 +135,16 @@ function* runTests(options) { }); }); + yield SpecialPowers.pushPrefEnv({set: [["general.useragent.locale", "es-ES"]]}); + yield extension.startup(); yield awaitFinish; yield extension.unload(); + yield SpecialPowers.popPrefEnv(); + let node = document.getElementById(pageActionId); is(node, null, "pageAction image removed from document"); @@ -182,6 +186,18 @@ add_task(function* testTabSwitchContext() { }, }, + "_locales/es_ES/messages.json": { + "popup": { + "message": "default.html", + "description": "Popup", + }, + + "title": { + "message": "T\u00edtulo", + "description": "Title", + }, + }, + "default.png": imageBuffer, "1.png": imageBuffer, "2.png": imageBuffer, @@ -191,16 +207,16 @@ add_task(function* testTabSwitchContext() { let details = [ {"icon": browser.runtime.getURL("default.png"), "popup": browser.runtime.getURL("default.html"), - "title": "Default Title \u263a"}, + "title": "Default T\u00edtulo \u263a"}, {"icon": browser.runtime.getURL("1.png"), "popup": browser.runtime.getURL("default.html"), - "title": "Default Title \u263a"}, + "title": "Default T\u00edtulo \u263a"}, {"icon": browser.runtime.getURL("2.png"), "popup": browser.runtime.getURL("2.html"), "title": "Title 2"}, {"icon": browser.runtime.getURL("2.png"), "popup": browser.runtime.getURL("2.html"), - "title": "Default Title \u263a"}, + "title": "Default T\u00edtulo \u263a"}, ]; let promiseTabLoad = details => { diff --git a/browser/locales/en-US/chrome/browser/downloads/downloads.dtd b/browser/locales/en-US/chrome/browser/downloads/downloads.dtd index cef56c7d4d2f..1217f43e44b0 100644 --- a/browser/locales/en-US/chrome/browser/downloads/downloads.dtd +++ b/browser/locales/en-US/chrome/browser/downloads/downloads.dtd @@ -62,8 +62,8 @@ - - + + + + + diff --git a/browser/themes/shared/jar.inc.mn b/browser/themes/shared/jar.inc.mn index 2e990b6a3a87..16e896972201 100644 --- a/browser/themes/shared/jar.inc.mn +++ b/browser/themes/shared/jar.inc.mn @@ -54,6 +54,7 @@ skin/classic/browser/customizableui/whimsy@2x.png (../shared/customizableui/whimsy@2x.png) skin/classic/browser/downloads/contentAreaDownloadsView.css (../shared/downloads/contentAreaDownloadsView.css) skin/classic/browser/downloads/download-blocked.svg (../shared/downloads/download-blocked.svg) + skin/classic/browser/downloads/menubutton-dropmarker.svg (../shared/downloads/menubutton-dropmarker.svg) skin/classic/browser/drm-icon.svg (../shared/drm-icon.svg) skin/classic/browser/filters.svg (../shared/filters.svg) skin/classic/browser/fullscreen/insecure.svg (../shared/fullscreen/insecure.svg) diff --git a/build/moz.configure/old.configure b/build/moz.configure/old.configure index 2b6c41d85b87..638e3686fdb7 100644 --- a/build/moz.configure/old.configure +++ b/build/moz.configure/old.configure @@ -199,7 +199,6 @@ def old_configure_options(*options): '--enable-memory-sanitizer', '--enable-mobile-optimize', '--enable-mozril-geoloc', - '--enable-necko-protocols', '--enable-necko-wifi', '--enable-negotiateauth', '--enable-nfc', diff --git a/devtools/client/shared/shim/Services.js b/devtools/client/shared/shim/Services.js index 2646f328e52a..9bcaaaeb1fb5 100644 --- a/devtools/client/shared/shim/Services.js +++ b/devtools/client/shared/shim/Services.js @@ -18,129 +18,11 @@ const NS_PREFBRANCH_PREFCHANGE_TOPIC_ID = "nsPref:changed"; // We prefix all our local storage items with this. const PREFIX = "Services.prefs:"; -/** - * Create a new preference object. - * - * @param {PrefBranch} branch the branch holding this preference - * @param {String} name the base name of this preference - * @param {String} fullName the fully-qualified name of this preference - */ -function Preference(branch, name, fullName) { - this.branch = branch; - this.name = name; - this.fullName = fullName; - this.defaultValue = null; - this.hasUserValue = false; - this.userValue = null; - this.type = null; -} - -Preference.prototype = { - /** - * Return this preference's current value. - * - * @return {Any} The current value of this preference. This may - * return a string, a number, or a boolean depending on the - * preference's type. - */ - get: function () { - if (this.hasUserValue) { - return this.userValue; - } - return this.defaultValue; - }, - - /** - * Set the preference's value. The new value is assumed to be a - * user value. After setting the value, this function emits a - * change notification. - * - * @param {Any} value the new value - */ - set: function (value) { - if (!this.hasUserValue || value !== this.userValue) { - this.userValue = value; - this.hasUserValue = true; - this.saveAndNotify(); - } - }, - - /** - * Set the default value for this preference, and emit a - * notification if this results in a visible change. - * - * @param {Any} value the new default value - */ - setDefault: function (value) { - if (this.defaultValue !== value) { - this.defaultValue = value; - if (!this.hasUserValue) { - this.saveAndNotify(); - } - } - }, - - /** - * If this preference has a user value, clear it. If a change was - * made, emit a change notification. - */ - clearUserValue: function () { - if (this.hasUserValue) { - this.userValue = null; - this.hasUserValue = false; - this.saveAndNotify(); - } - }, - - /** - * Helper function to write the preference's value to local storage - * and then emit a change notification. - */ - saveAndNotify: function () { - let store = { - type: this.type, - defaultValue: this.defaultValue, - hasUserValue: this.hasUserValue, - userValue: this.userValue, - }; - - localStorage.setItem(PREFIX + this.fullName, JSON.stringify(store)); - this.branch._notify(this.name); - }, - - /** - * Change this preference's value without writing it back to local - * storage. This is used to handle changes to local storage that - * were made externally. - * - * @param {Number} type one of the PREF_* values - * @param {Any} userValue the user value to use if the pref does not exist - * @param {Any} defaultValue the default value to use if the pref - * does not exist - * @param {Boolean} hasUserValue if a new pref is created, whether - * the default value is also a user value - * @param {Object} store the new value of the preference. It should - * be of the form {type, defaultValue, hasUserValue, userValue}; - * where |type| is one of the PREF_* type constants; |defaultValue| - * and |userValue| are the default and user values, respectively; - * and |hasUserValue| is a boolean indicating whether the user value - * is valid - */ - storageUpdated: function (type, userValue, hasUserValue, defaultValue) { - this.type = type; - this.defaultValue = defaultValue; - this.hasUserValue = hasUserValue; - this.userValue = userValue; - // There's no need to write this back to local storage, since it - // came from there; and this avoids infinite event loops. - this.branch._notify(this.name); - }, -}; - /** * Create a new preference branch. This object conforms largely to * nsIPrefBranch and nsIPrefService, though it only implements the - * subset needed by devtools. + * subset needed by devtools. A preference branch can hold child + * preferences while also holding a preference value itself. * * @param {PrefBranch} parent the parent branch, or null for the root * branch. @@ -154,6 +36,12 @@ function PrefBranch(parent, name, fullName) { this._observers = {}; this._children = {}; + // Properties used when this branch has a value as well. + this._defaultValue = null; + this._hasUserValue = false; + this._userValue = null; + this._type = PREF_INVALID; + if (!parent) { this._initializeRoot(); } @@ -172,16 +60,16 @@ PrefBranch.prototype = { /** @see nsIPrefBranch.getPrefType. */ getPrefType: function (prefName) { - return this._findPref(prefName).type; + return this._findPref(prefName)._type; }, /** @see nsIPrefBranch.getBoolPref. */ getBoolPref: function (prefName) { let thePref = this._findPref(prefName); - if (thePref.type !== PREF_BOOL) { + if (thePref._type !== PREF_BOOL) { throw new Error(`${prefName} does not have bool type`); } - return thePref.get(); + return thePref._get(); }, /** @see nsIPrefBranch.setBoolPref. */ @@ -190,19 +78,19 @@ PrefBranch.prototype = { throw new Error("non-bool passed to setBoolPref"); } let thePref = this._findOrCreatePref(prefName, value, true, value); - if (thePref.type !== PREF_BOOL) { + if (thePref._type !== PREF_BOOL) { throw new Error(`${prefName} does not have bool type`); } - thePref.set(value); + thePref._set(value); }, /** @see nsIPrefBranch.getCharPref. */ getCharPref: function (prefName) { let thePref = this._findPref(prefName); - if (thePref.type !== PREF_STRING) { + if (thePref._type !== PREF_STRING) { throw new Error(`${prefName} does not have string type`); } - return thePref.get(); + return thePref._get(); }, /** @see nsIPrefBranch.setCharPref. */ @@ -211,19 +99,19 @@ PrefBranch.prototype = { throw new Error("non-string passed to setCharPref"); } let thePref = this._findOrCreatePref(prefName, value, true, value); - if (thePref.type !== PREF_STRING) { + if (thePref._type !== PREF_STRING) { throw new Error(`${prefName} does not have string type`); } - thePref.set(value); + thePref._set(value); }, /** @see nsIPrefBranch.getIntPref. */ getIntPref: function (prefName) { let thePref = this._findPref(prefName); - if (thePref.type !== PREF_INT) { + if (thePref._type !== PREF_INT) { throw new Error(`${prefName} does not have int type`); } - return thePref.get(); + return thePref._get(); }, /** @see nsIPrefBranch.setIntPref. */ @@ -232,22 +120,22 @@ PrefBranch.prototype = { throw new Error("non-number passed to setIntPref"); } let thePref = this._findOrCreatePref(prefName, value, true, value); - if (thePref.type !== PREF_INT) { + if (thePref._type !== PREF_INT) { throw new Error(`${prefName} does not have int type`); } - thePref.set(value); + thePref._set(value); }, /** @see nsIPrefBranch.clearUserPref */ clearUserPref: function (prefName) { let thePref = this._findPref(prefName); - thePref.clearUserValue(); + thePref._clearUserValue(); }, /** @see nsIPrefBranch.prefHasUserValue */ prefHasUserValue: function (prefName) { let thePref = this._findPref(prefName); - return thePref.hasUserValue; + return thePref._hasUserValue; }, /** @see nsIPrefBranch.addObserver */ @@ -294,6 +182,106 @@ PrefBranch.prototype = { return this._findPref(prefRoot); }, + /** + * Return this preference's current value. + * + * @return {Any} The current value of this preference. This may + * return a string, a number, or a boolean depending on the + * preference's type. + */ + _get: function () { + if (this._hasUserValue) { + return this._userValue; + } + return this._defaultValue; + }, + + /** + * Set the preference's value. The new value is assumed to be a + * user value. After setting the value, this function emits a + * change notification. + * + * @param {Any} value the new value + */ + _set: function (value) { + if (!this._hasUserValue || value !== this._userValue) { + this._userValue = value; + this._hasUserValue = true; + this._saveAndNotify(); + } + }, + + /** + * Set the default value for this preference, and emit a + * notification if this results in a visible change. + * + * @param {Any} value the new default value + */ + _setDefault: function (value) { + if (this._defaultValue !== value) { + this._defaultValue = value; + if (!this._hasUserValue) { + this._saveAndNotify(); + } + } + }, + + /** + * If this preference has a user value, clear it. If a change was + * made, emit a change notification. + */ + _clearUserValue: function () { + if (this._hasUserValue) { + this._userValue = null; + this._hasUserValue = false; + this._saveAndNotify(); + } + }, + + /** + * Helper function to write the preference's value to local storage + * and then emit a change notification. + */ + _saveAndNotify: function () { + let store = { + type: this._type, + defaultValue: this._defaultValue, + hasUserValue: this._hasUserValue, + userValue: this._userValue, + }; + + localStorage.setItem(PREFIX + this.fullName, JSON.stringify(store)); + this._parent._notify(this._name); + }, + + /** + * Change this preference's value without writing it back to local + * storage. This is used to handle changes to local storage that + * were made externally. + * + * @param {Number} type one of the PREF_* values + * @param {Any} userValue the user value to use if the pref does not exist + * @param {Any} defaultValue the default value to use if the pref + * does not exist + * @param {Boolean} hasUserValue if a new pref is created, whether + * the default value is also a user value + * @param {Object} store the new value of the preference. It should + * be of the form {type, defaultValue, hasUserValue, userValue}; + * where |type| is one of the PREF_* type constants; |defaultValue| + * and |userValue| are the default and user values, respectively; + * and |hasUserValue| is a boolean indicating whether the user value + * is valid + */ + _storageUpdated: function (type, userValue, hasUserValue, defaultValue) { + this._type = type; + this._defaultValue = defaultValue; + this._hasUserValue = hasUserValue; + this._userValue = userValue; + // There's no need to write this back to local storage, since it + // came from there; and this avoids infinite event loops. + this._parent._notify(this._name); + }, + /** * Helper function to find either a Preference or PrefBranch object * given its name. If the name is not found, throws an exception. @@ -378,36 +366,34 @@ PrefBranch.prototype = { * the default value is also a user value */ _findOrCreatePref: function (keyName, userValue, hasUserValue, defaultValue) { - let branchName = keyName.split("."); - let prefName = branchName.pop(); + let branch = this._createBranch(keyName.split(".")); - let branch = this._createBranch(branchName); - if (!(prefName in branch._children)) { - if (hasUserValue && typeof (userValue) !== typeof (defaultValue)) { - throw new Error("inconsistent values when creating " + keyName); - } - - let type; - switch (typeof (defaultValue)) { - case "boolean": - type = PREF_BOOL; - break; - case "number": - type = PREF_INT; - break; - case "string": - type = PREF_STRING; - break; - default: - throw new Error("unhandled argument type: " + typeof (defaultValue)); - } - - let thePref = new Preference(branch, prefName, keyName); - thePref.storageUpdated(type, userValue, hasUserValue, defaultValue); - branch._children[prefName] = thePref; + if (hasUserValue && typeof (userValue) !== typeof (defaultValue)) { + throw new Error("inconsistent values when creating " + keyName); } - return branch._children[prefName]; + let type; + switch (typeof (defaultValue)) { + case "boolean": + type = PREF_BOOL; + break; + case "number": + type = PREF_INT; + break; + case "string": + type = PREF_STRING; + break; + default: + throw new Error("unhandled argument type: " + typeof (defaultValue)); + } + + if (branch._type === PREF_INVALID) { + branch._storageUpdated(type, userValue, hasUserValue, defaultValue); + } else if (branch._type !== type) { + throw new Error("attempt to change type of pref " + keyName); + } + + return branch; }, /** @@ -432,7 +418,7 @@ PrefBranch.prototype = { this._findOrCreatePref(event.key, userValue, hasUserValue, defaultValue); } else { let thePref = this._findPref(event.key); - thePref.storageUpdated(type, userValue, hasUserValue, defaultValue); + thePref._storageUpdated(type, userValue, hasUserValue, defaultValue); } }, @@ -592,7 +578,7 @@ const Services = { */ function pref(name, value) { let thePref = Services.prefs._findOrCreatePref(name, value, true, value); - thePref.setDefault(value); + thePref._setDefault(value); } module.exports = Services; diff --git a/devtools/client/shared/shim/test/test_service_prefs.html b/devtools/client/shared/shim/test/test_service_prefs.html index 51cab8364d33..fbf64451f21a 100644 --- a/devtools/client/shared/shim/test/test_service_prefs.html +++ b/devtools/client/shared/shim/test/test_service_prefs.html @@ -218,6 +218,9 @@ function do_tests() { "someotherstring": true }, "pref worked"); + // Regression test for bug 1296427. + pref("devtools.hud.loglimit", 1000); + pref("devtools.hud.loglimit.network", 1000); // Clean up. localStorage.clear(); diff --git a/devtools/client/themes/memory.css b/devtools/client/themes/memory.css index 328cde44b84d..ba863db7adb8 100644 --- a/devtools/client/themes/memory.css +++ b/devtools/client/themes/memory.css @@ -395,6 +395,10 @@ html, body, #app, #memory-tool { padding-inline-end: 5px; } +.children-pointer:dir(rtl) { + transform: scaleX(-1); +} + /** * Heap tree view columns */ diff --git a/devtools/client/webconsole/new-console-output/components/variables-view-link.js b/devtools/client/webconsole/new-console-output/components/variables-view-link.js index ffdd847c3338..2c4ae11b82ec 100644 --- a/devtools/client/webconsole/new-console-output/components/variables-view-link.js +++ b/devtools/client/webconsole/new-console-output/components/variables-view-link.js @@ -27,7 +27,6 @@ function VariablesViewLink(props) { onClick: openVariablesView.bind(null, object), className: "cm-variable", draggable: false, - href: "#" }, children) ); } diff --git a/devtools/client/webconsole/new-console-output/constants.js b/devtools/client/webconsole/new-console-output/constants.js index edcdd7d381da..7a3ea2ab7af4 100644 --- a/devtools/client/webconsole/new-console-output/constants.js +++ b/devtools/client/webconsole/new-console-output/constants.js @@ -44,7 +44,10 @@ const chromeRDPEnums = { // Undocumented in Chrome RDP, but is used for evaluation results. RESULT: "result", // Undocumented in Chrome RDP, but is used for input. - COMMAND: "command" + COMMAND: "command", + // Undocumented in Chrome RDP, but is used for messages that should not + // output anything (e.g. `console.time()` calls). + NULL_MESSAGE: "nullMessage", }, MESSAGE_LEVEL: { LOG: "log", diff --git a/devtools/client/webconsole/new-console-output/reducers/messages.js b/devtools/client/webconsole/new-console-output/reducers/messages.js index 428905059dfb..622096c9ba58 100644 --- a/devtools/client/webconsole/new-console-output/reducers/messages.js +++ b/devtools/client/webconsole/new-console-output/reducers/messages.js @@ -21,7 +21,11 @@ function messages(state = new MessageState(), action) { case constants.MESSAGE_ADD: let newMessage = action.message; - if (newMessage.type === "clear") { + if (newMessage.type === constants.MESSAGE_TYPE.NULL_MESSAGE) { + return state; + } + + if (newMessage.type === constants.MESSAGE_TYPE.CLEAR) { return state.set("messagesById", Immutable.List([newMessage])); } diff --git a/devtools/client/webconsole/new-console-output/test/components/console-api-call.test.js b/devtools/client/webconsole/new-console-output/test/components/console-api-call.test.js index 5da278a9248d..282bc39355e4 100644 --- a/devtools/client/webconsole/new-console-output/test/components/console-api-call.test.js +++ b/devtools/client/webconsole/new-console-output/test/components/console-api-call.test.js @@ -46,6 +46,27 @@ describe("ConsoleAPICall component:", () => { expect(messageBody.textContent).toBe(message.messageText); }); }); + + describe("console.time", () => { + it("does not show anything", () => { + const message = stubConsoleMessages.get("console.time('bar')"); + const rendered = renderComponent(ConsoleApiCall, {message, onViewSourceInDebugger}); + + const messageBody = getMessageBody(rendered); + expect(messageBody.textContent).toBe(""); + }); + }); + + describe("console.timeEnd", () => { + it("renders as expected", () => { + const message = stubConsoleMessages.get("console.timeEnd('bar')"); + const rendered = renderComponent(ConsoleApiCall, {message, onViewSourceInDebugger}); + + const messageBody = getMessageBody(rendered); + expect(messageBody.textContent).toBe(message.messageText); + expect(messageBody.textContent).toMatch(/^bar: \d+(\.\d+)?ms$/); + }); + }); }); function getMessageBody(rendered) { @@ -56,4 +77,4 @@ function getMessageBody(rendered) { function getRepeatNode(rendered) { const repeatPath = "span > span.message-flex-body > span.message-body.devtools-monospace + span.message-repeats"; return rendered.querySelectorAll(repeatPath); -} \ No newline at end of file +} diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_console_api.js b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_console_api.js index 4e73c760eb7c..08261822970e 100644 --- a/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_console_api.js +++ b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/browser_webconsole_update_stubs_console_api.js @@ -12,11 +12,10 @@ const TEST_URI = "http://example.com/browser/devtools/client/webconsole/new-cons let stubs = []; -snippets.forEach((code, key) => { - add_task(function* () { - let tempFilePath = OS.Path.join(`${BASE_PATH}/stub-generators`, "test-tempfile.js"); +add_task(function* () { + let tempFilePath = OS.Path.join(`${BASE_PATH}/stub-generators`, "test-tempfile.js"); + for (var [key, {keys, code}] of snippets) { OS.File.writeAtomic(tempFilePath, `function triggerPacket() {${code}}`); - let toolbox = yield openNewTabAndToolbox(TEST_URI, "webconsole"); let hud = toolbox.getCurrentPanel().hud; let {ui} = hud; @@ -24,17 +23,23 @@ snippets.forEach((code, key) => { ok(ui.jsterm, "jsterm exists"); ok(ui.newConsoleOutput, "newConsoleOutput exists"); - toolbox.target.client.addListener("consoleAPICall", (type, res) => { - stubs.push(formatStub(key, res)); - if (stubs.length == snippets.size) { - let filePath = OS.Path.join(`${BASE_PATH}/stubs`, "consoleApi.js"); - OS.File.writeAtomic(filePath, formatFile(stubs)); - OS.File.writeAtomic(tempFilePath, ""); - } + let received = new Promise(resolve => { + let i = 0; + toolbox.target.client.addListener("consoleAPICall", (type, res) => { + stubs.push(formatStub(keys[i], res)); + if(++i === keys.length ){ + resolve(); + } + }); }); yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function() { content.wrappedJSObject.triggerPacket(); }); - }); + + yield received; + } + let filePath = OS.Path.join(`${BASE_PATH}/stubs`, "consoleApi.js"); + OS.File.writeAtomic(filePath, formatFile(stubs)); + OS.File.writeAtomic(tempFilePath, ""); }); diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/stub-snippets.js b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/stub-snippets.js index 73988352096e..92b2df4d41ff 100644 --- a/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/stub-snippets.js +++ b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/stub-snippets.js @@ -15,10 +15,12 @@ const consoleApiCommands = [ "console.count('bar')", ]; -let consoleApi = new Map(consoleApiCommands.map(cmd => [cmd, cmd])); +let consoleApi = new Map(consoleApiCommands.map( + cmd => [cmd, {keys: [cmd], code: cmd}])); -consoleApi.set("console.trace()", -` +consoleApi.set("console.trace()", { + keys: ["console.trace()"], + code: ` function bar() { console.trace() } @@ -27,13 +29,14 @@ function foo() { } foo() -`); +`}); -consoleApi.set("console.time()", -` -console.time() -console.timeEnd() -`); +consoleApi.set("console.time('bar')", { + keys: ["console.time('bar')", "console.timeEnd('bar')"], + code: ` +console.time("bar"); +console.timeEnd("bar"); +`}); // Evaluation Result diff --git a/devtools/client/webconsole/new-console-output/test/fixtures/stubs/consoleApi.js b/devtools/client/webconsole/new-console-output/test/fixtures/stubs/consoleApi.js index 96fa5b71c275..bd959ff1c9d0 100644 --- a/devtools/client/webconsole/new-console-output/test/fixtures/stubs/consoleApi.js +++ b/devtools/client/webconsole/new-console-output/test/fixtures/stubs/consoleApi.js @@ -196,5 +196,41 @@ stubConsoleMessages.set("console.trace()", new ConsoleMessage({ } })); +stubConsoleMessages.set("console.time('bar')", new ConsoleMessage({ + "id": "1", + "allowRepeating": true, + "source": "console-api", + "type": "nullMessage", + "level": "log", + "messageText": null, + "parameters": null, + "repeat": 1, + "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"nullMessage\",\"level\":\"log\",\"messageText\":null,\"parameters\":null,\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js\",\"line\":2,\"column\":1}}", + "stacktrace": null, + "frame": { + "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js", + "line": 2, + "column": 1 + } +})); + +stubConsoleMessages.set("console.timeEnd('bar')", new ConsoleMessage({ + "id": "1", + "allowRepeating": true, + "source": "console-api", + "type": "timeEnd", + "level": "log", + "messageText": "bar: 3.87ms", + "parameters": null, + "repeat": 1, + "repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"timeEnd\",\"level\":\"log\",\"messageText\":\"bar: 3.87ms\",\"parameters\":null,\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js\",\"line\":3,\"column\":1}}", + "stacktrace": null, + "frame": { + "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js", + "line": 3, + "column": 1 + } +})); + module.exports = stubConsoleMessages \ No newline at end of file diff --git a/devtools/client/webconsole/new-console-output/test/store/messages.test.js b/devtools/client/webconsole/new-console-output/test/store/messages.test.js index 44d26114a415..81644bf9194d 100644 --- a/devtools/client/webconsole/new-console-output/test/store/messages.test.js +++ b/devtools/client/webconsole/new-console-output/test/store/messages.test.js @@ -96,4 +96,14 @@ describe("Message reducer:", () => { expect(messages.first().parameters[0]).toBe(`message num 2`); expect(messages.last().parameters[0]).toBe(`message num ${logLimit + 1}`); }); + + it("does not add null messages to the store", () => { + const { dispatch, getState } = setupStore([]); + + const message = stubConsoleMessages.get("console.time('bar')"); + dispatch(actions.messageAdd(message)); + + const messages = getAllMessages(getState()); + expect(messages.size).toBe(0); + }); }); diff --git a/devtools/client/webconsole/new-console-output/utils/messages.js b/devtools/client/webconsole/new-console-output/utils/messages.js index bbc022344de1..b6a8b292b24e 100644 --- a/devtools/client/webconsole/new-console-output/utils/messages.js +++ b/devtools/client/webconsole/new-console-output/utils/messages.js @@ -45,6 +45,7 @@ function transformPacket(packet) { let type = message.level; let level = getLevelFromType(type); let messageText = null; + const timer = message.timer; // Special per-type conversion. switch (type) { @@ -60,6 +61,23 @@ function transformPacket(packet) { messageText = `${label}: ${counter.count}`; parameters = null; break; + case "time": + // We don't show anything for console.time calls to match Chrome's behaviour. + parameters = null; + type = MESSAGE_TYPE.NULL_MESSAGE; + break; + case "timeEnd": + parameters = null; + if (timer) { + // We show the duration to users when calls console.timeEnd() is called, + // if corresponding console.time() was called before. + let duration = Math.round(timer.duration * 100) / 100; + messageText = l10n.getFormatStr("timeEnd", [timer.name, duration]); + } else { + // If the `timer` property does not exists, we don't output anything. + type = MESSAGE_TYPE.NULL_MESSAGE; + } + break; } const frame = { diff --git a/dom/canvas/WebGLContext.cpp b/dom/canvas/WebGLContext.cpp index 349302135aef..e23aa1e1ce4d 100644 --- a/dom/canvas/WebGLContext.cpp +++ b/dom/canvas/WebGLContext.cpp @@ -125,7 +125,6 @@ WebGLContext::WebGLContext() , mNeedsFakeNoDepth(false) , mNeedsFakeNoStencil(false) , mNeedsEmulatedLoneDepthStencil(false) - , mVRPresentationActive(false) { mGeneration = 0; mInvalidated = false; @@ -1307,9 +1306,8 @@ public: HTMLCanvasElement* canvas = userdata->mCanvas; WebGLContext* webgl = static_cast(canvas->GetContextAtIndex(0)); - // Present our screenbuffer, if needed. - webgl->PresentScreenBuffer(); - webgl->mDrawCallsSinceLastFlush = 0; + // Prepare the context for composition + webgl->BeginComposition(); } /** DidTransactionCallback gets called by the Layers code everytime the WebGL canvas gets composite, @@ -1320,10 +1318,8 @@ public: HTMLCanvasElement* canvas = userdata->mCanvas; WebGLContext* webgl = static_cast(canvas->GetContextAtIndex(0)); - // Mark ourselves as no longer invalidated. - webgl->MarkContextClean(); - - webgl->UpdateLastUseIndex(); + // Clean up the context after composition + webgl->EndComposition(); } private: @@ -1612,6 +1608,24 @@ WebGLContext::PresentScreenBuffer() return true; } +// Prepare the context for capture before compositing +void +WebGLContext::BeginComposition() +{ + // Present our screenbuffer, if needed. + PresentScreenBuffer(); + mDrawCallsSinceLastFlush = 0; +} + +// Clean up the context after captured for compositing +void +WebGLContext::EndComposition() +{ + // Mark ourselves as no longer invalidated. + MarkContextClean(); + UpdateLastUseIndex(); +} + void WebGLContext::DummyReadFramebufferOperation(const char* funcName) { @@ -2340,40 +2354,43 @@ WebGLContext::GetUnpackSize(bool isFunc3D, uint32_t width, uint32_t height, already_AddRefed WebGLContext::GetVRFrame() { - VRManagerChild *vrmc = VRManagerChild::Get(); - if (!vrmc) { - return nullptr; - } - - PresentScreenBuffer(); - mDrawCallsSinceLastFlush = 0; - - MarkContextClean(); - UpdateLastUseIndex(); - - gl::GLScreenBuffer* screen = gl->Screen(); - if (!screen) { - return nullptr; - } - - RefPtr sharedSurface = screen->Front(); - if (!sharedSurface) { - return nullptr; - } - - if (sharedSurface && sharedSurface->GetAllocator() != vrmc) { - RefPtr dest = - screen->Factory()->NewTexClient(sharedSurface->GetSize()); - if (!dest) { - return nullptr; + VRManagerChild* vrmc = VRManagerChild::Get(); + if (!vrmc) { + return nullptr; } - gl::SharedSurface* destSurf = dest->Surf(); - destSurf->ProducerAcquire(); - SharedSurface::ProdCopy(sharedSurface->Surf(), dest->Surf(), screen->Factory()); - destSurf->ProducerRelease(); - return dest.forget(); - } + /** + * Swap buffers as though composition has occurred. + * We will then share the resulting front buffer to be submitted to the VR + * compositor. + */ + BeginComposition(); + EndComposition(); + + gl::GLScreenBuffer* screen = gl->Screen(); + if (!screen) { + return nullptr; + } + + RefPtr sharedSurface = screen->Front(); + if (!sharedSurface) { + return nullptr; + } + + if (sharedSurface && sharedSurface->GetAllocator() != vrmc) { + RefPtr dest = + screen->Factory()->NewTexClient(sharedSurface->GetSize()); + if (!dest) { + return nullptr; + } + gl::SharedSurface* destSurf = dest->Surf(); + destSurf->ProducerAcquire(); + SharedSurface::ProdCopy(sharedSurface->Surf(), dest->Surf(), + screen->Factory()); + destSurf->ProducerRelease(); + + return dest.forget(); + } return sharedSurface.forget(); } @@ -2381,26 +2398,25 @@ WebGLContext::GetVRFrame() bool WebGLContext::StartVRPresentation() { - VRManagerChild *vrmc = VRManagerChild::Get(); - if (!vrmc) { - return false; - } - gl::GLScreenBuffer* screen = gl->Screen(); - if (!screen) { - return false; - } - gl::SurfaceCaps caps = screen->mCaps; + VRManagerChild* vrmc = VRManagerChild::Get(); + if (!vrmc) { + return false; + } + gl::GLScreenBuffer* screen = gl->Screen(); + if (!screen) { + return false; + } + gl::SurfaceCaps caps = screen->mCaps; - UniquePtr factory = - gl::GLScreenBuffer::CreateFactory(gl, - caps, - vrmc, - vrmc->GetBackendType(), - TextureFlags::ORIGIN_BOTTOM_LEFT); + UniquePtr factory = + gl::GLScreenBuffer::CreateFactory(gl, + caps, + vrmc, + vrmc->GetBackendType(), + TextureFlags::ORIGIN_BOTTOM_LEFT); - screen->Morph(Move(factory)); - mVRPresentationActive = true; - return true; + screen->Morph(Move(factory)); + return true; } //////////////////////////////////////////////////////////////////////////////// diff --git a/dom/canvas/WebGLContext.h b/dom/canvas/WebGLContext.h index 0bfdbb088671..59b2969e81f5 100644 --- a/dom/canvas/WebGLContext.h +++ b/dom/canvas/WebGLContext.h @@ -355,6 +355,11 @@ public: bool PresentScreenBuffer(); + // Prepare the context for capture before compositing + void BeginComposition(); + // Clean up the context after captured for compositing + void EndComposition(); + // a number that increments every time we have an event that causes // all context resources to be lost. uint32_t Generation() { return mGeneration.value(); } @@ -1517,7 +1522,6 @@ protected: bool mNeedsFakeNoDepth; bool mNeedsFakeNoStencil; bool mNeedsEmulatedLoneDepthStencil; - bool mVRPresentationActive; bool HasTimestampBits() const; diff --git a/dom/events/DragEvent.cpp b/dom/events/DragEvent.cpp index 2973155ff76c..954aafcb524f 100644 --- a/dom/events/DragEvent.cpp +++ b/dom/events/DragEvent.cpp @@ -59,7 +59,7 @@ DragEvent::InitDragEvent(const nsAString& aType, aView, aDetail, aScreenX, aScreenY, aClientX, aClientY, aCtrlKey, aAltKey, aShiftKey, aMetaKey, aButton, aRelatedTarget); - if (mEventIsInternal && mEvent) { + if (mEventIsInternal) { mEvent->AsDragEvent()->mDataTransfer = aDataTransfer; } } diff --git a/dom/html/HTMLMediaElement.cpp b/dom/html/HTMLMediaElement.cpp index 061266bab29d..b2c0b0d8b95b 100644 --- a/dom/html/HTMLMediaElement.cpp +++ b/dom/html/HTMLMediaElement.cpp @@ -907,6 +907,7 @@ void HTMLMediaElement::ShutdownDecoder() { RemoveMediaElementFromURITable(); NS_ASSERTION(mDecoder, "Must have decoder to shut down"); + mWaitingForKeyListener.DisconnectIfExists(); mDecoder->Shutdown(); mDecoder = nullptr; } @@ -983,6 +984,7 @@ void HTMLMediaElement::AbortExistingLoads() #ifdef MOZ_EME mPendingEncryptedInitData.mInitDatas.Clear(); #endif // MOZ_EME + mWaitingForKey = false; mSourcePointer = nullptr; mTags = nullptr; @@ -2509,6 +2511,7 @@ HTMLMediaElement::HTMLMediaElement(already_AddRefed& aNo mMediaSecurityVerified(false), mCORSMode(CORS_NONE), mIsEncrypted(false), + mWaitingForKey(false), mDownloadSuspendedByCache(false, "HTMLMediaElement::mDownloadSuspendedByCache"), mAudioChannelVolume(1.0), mPlayingThroughTheAudioChannel(false), @@ -3439,6 +3442,13 @@ nsresult HTMLMediaElement::FinishDecoderSetup(MediaDecoder* aDecoder, } #endif + MediaEventSource* waitingForKeyProducer = mDecoder->WaitingForKeyEvent(); + // Not every decoder will produce waitingForKey events, only add ones that can + if (waitingForKeyProducer) { + mWaitingForKeyListener = waitingForKeyProducer->Connect( + AbstractThread::MainThread(), this, &HTMLMediaElement::CannotDecryptWaitingForKey); + } + if (mChannelLoader) { mChannelLoader->Done(); mChannelLoader = nullptr; @@ -4501,6 +4511,7 @@ void HTMLMediaElement::ChangeReadyState(nsMediaReadyState aState) if (oldState < nsIDOMHTMLMediaElement::HAVE_FUTURE_DATA && mReadyState >= nsIDOMHTMLMediaElement::HAVE_FUTURE_DATA && IsPotentiallyPlaying()) { + mWaitingForKey = false; DispatchAsyncEvent(NS_LITERAL_STRING("playing")); } @@ -5886,6 +5897,27 @@ HTMLMediaElement::GetTopLevelPrincipal() } #endif // MOZ_EME +void +HTMLMediaElement::CannotDecryptWaitingForKey() +{ + // See: http://w3c.github.io/encrypted-media/#dom-evt-waitingforkey + // Spec: 7.5.4 Queue a "waitingforkey" Event + // Spec: 1. Let the media element be the specified HTMLMediaElement object. + + // Note, existing code will handle the ready state of this element, as + // such this function does not handle changing or checking mReadyState. + + // Spec: 2. If the media element's waiting for key value is true, abort these steps. + if (!mWaitingForKey) { + // Spec: 3. Set the media element's waiting for key value to true. + // Spec: 4. Queue a task to fire a simple event named waitingforkey at the media element. + DispatchAsyncEvent(NS_LITERAL_STRING("waitingforkey")); + mWaitingForKey = true; + // No need to explicitly suspend playback, it happens automatically when + // it's starving for decoded frames. + } +} + NS_IMETHODIMP HTMLMediaElement::WindowAudioCaptureChanged(bool aCapture) { MOZ_ASSERT(mAudioChannelAgent); diff --git a/dom/html/HTMLMediaElement.h b/dom/html/HTMLMediaElement.h index 78c43bd2a8e5..f28aaab05c4a 100644 --- a/dom/html/HTMLMediaElement.h +++ b/dom/html/HTMLMediaElement.h @@ -641,6 +641,8 @@ public: bool ContainsRestrictedContent(); #endif // MOZ_EME + void CannotDecryptWaitingForKey(); + bool MozAutoplayEnabled() const { return mAutoplayEnabled; @@ -1537,6 +1539,14 @@ protected: // True if the media has encryption information. bool mIsEncrypted; + // True when the CDM cannot decrypt the current block, and the + // waitingforkey event has been fired. Back to false when keys have become + // available and we can advance the current playback position. + bool mWaitingForKey; + + // Listens for waitingForKey events from the owned decoder. + MediaEventListener mWaitingForKeyListener; + #ifdef MOZ_EME // Init Data that needs to be sent in 'encrypted' events in MetadataLoaded(). EncryptionInfo mPendingEncryptedInitData; diff --git a/dom/media/AbstractMediaDecoder.h b/dom/media/AbstractMediaDecoder.h index 8aa63867adea..a8af770055e8 100644 --- a/dom/media/AbstractMediaDecoder.h +++ b/dom/media/AbstractMediaDecoder.h @@ -72,6 +72,18 @@ public: return nullptr; } + // Notify the media decoder that a decryption key is required before emitting + // further output. This only needs to be overridden for decoders that expect + // encryption, such as the MediaSource decoder. + virtual void NotifyWaitingForKey() {} + + // Return an event that will be notified when a decoder is waiting for a + // decryption key before it can return more output. + virtual MediaEventSource* WaitingForKeyEvent() + { + return nullptr; + } + protected: virtual void UpdateEstimatedMediaDuration(int64_t aDuration) {}; public: diff --git a/dom/media/MediaDecoderStateMachine.cpp b/dom/media/MediaDecoderStateMachine.cpp index b5726b5dc52f..f60b4a81b0a0 100644 --- a/dom/media/MediaDecoderStateMachine.cpp +++ b/dom/media/MediaDecoderStateMachine.cpp @@ -1063,21 +1063,46 @@ MediaDecoderStateMachine::ToStateStr() return ToStateStr(mState); } -void MediaDecoderStateMachine::SetState(State aState) +void +MediaDecoderStateMachine::SetState(State aState) { MOZ_ASSERT(OnTaskQueue()); if (mState == aState) { return; } - DECODER_LOG("Change machine state from %s to %s", - ToStateStr(), ToStateStr(aState)); + DECODER_LOG("MDSM state: %s -> %s", ToStateStr(), ToStateStr(aState)); + + ExitState(mState); mState = aState; + EnterState(mState); +} - mIsShutdown = mState == DECODER_STATE_ERROR || mState == DECODER_STATE_SHUTDOWN; +void +MediaDecoderStateMachine::ExitState(State aState) +{ + MOZ_ASSERT(OnTaskQueue()); + switch (aState) { + case DECODER_STATE_COMPLETED: + mSentPlaybackEndedEvent = false; + break; + default: + break; + } +} - // Clear state-scoped state. - mSentPlaybackEndedEvent = false; +void +MediaDecoderStateMachine::EnterState(State aState) +{ + MOZ_ASSERT(OnTaskQueue()); + switch (aState) { + case DECODER_STATE_ERROR: + case DECODER_STATE_SHUTDOWN: + mIsShutdown = true; + break; + default: + break; + } } void MediaDecoderStateMachine::VolumeChanged() diff --git a/dom/media/MediaDecoderStateMachine.h b/dom/media/MediaDecoderStateMachine.h index 1618c5175596..0d1abcc10d1b 100644 --- a/dom/media/MediaDecoderStateMachine.h +++ b/dom/media/MediaDecoderStateMachine.h @@ -372,6 +372,8 @@ protected: virtual ~MediaDecoderStateMachine(); void SetState(State aState); + void ExitState(State aState); + void EnterState(State aState); void BufferedRangeUpdated(); diff --git a/dom/media/MediaFormatReader.cpp b/dom/media/MediaFormatReader.cpp index 2b94e1e6f6bb..bb68f4a1a45f 100644 --- a/dom/media/MediaFormatReader.cpp +++ b/dom/media/MediaFormatReader.cpp @@ -1542,6 +1542,14 @@ MediaFormatReader::DropDecodedSamples(TrackType aTrack) } } +void +MediaFormatReader::WaitingForKey(TrackType aTrack) +{ + if (mDecoder) { + mDecoder->NotifyWaitingForKey(); + } +} + void MediaFormatReader::SkipVideoDemuxToNextKeyFrame(media::TimeUnit aTimeThreshold) { diff --git a/dom/media/MediaFormatReader.h b/dom/media/MediaFormatReader.h index e279a94abda4..86782071e496 100644 --- a/dom/media/MediaFormatReader.h +++ b/dom/media/MediaFormatReader.h @@ -185,6 +185,7 @@ private: void Reset(TrackType aTrack); void DrainComplete(TrackType aTrack); void DropDecodedSamples(TrackType aTrack); + void WaitingForKey(TrackType aTrack); bool ShouldSkip(bool aSkipToNextKeyframe, media::TimeUnit aTimeThreshold); @@ -219,6 +220,9 @@ private: bool OnReaderTaskQueue() override { return mReader->OnTaskQueue(); } + void WaitingForKey() override { + mReader->WaitingForKey(mType); + } private: MediaFormatReader* mReader; diff --git a/dom/media/mediasource/MediaSourceDecoder.cpp b/dom/media/mediasource/MediaSourceDecoder.cpp index 15f07537d408..14216b1f3afb 100644 --- a/dom/media/mediasource/MediaSourceDecoder.cpp +++ b/dom/media/mediasource/MediaSourceDecoder.cpp @@ -316,6 +316,18 @@ MediaSourceDecoder::CanPlayThrough() return GetBuffered().Contains(ClampIntervalToEnd(interval)); } +void +MediaSourceDecoder::NotifyWaitingForKey() +{ + mWaitingForKeyEvent.Notify(); +} + +MediaEventSource* +MediaSourceDecoder::WaitingForKeyEvent() +{ + return &mWaitingForKeyEvent; +} + TimeInterval MediaSourceDecoder::ClampIntervalToEnd(const TimeInterval& aInterval) { diff --git a/dom/media/mediasource/MediaSourceDecoder.h b/dom/media/mediasource/MediaSourceDecoder.h index 6b880b2e9c5d..a873782f812c 100644 --- a/dom/media/mediasource/MediaSourceDecoder.h +++ b/dom/media/mediasource/MediaSourceDecoder.h @@ -80,6 +80,10 @@ public: MediaDecoderOwner::NextFrameStatus NextFrameBufferedStatus() override; bool CanPlayThrough() override; + void NotifyWaitingForKey() override; + + MediaEventSource* WaitingForKeyEvent() override; + private: void DoSetMediaSourceDuration(double aDuration); media::TimeInterval ClampIntervalToEnd(const media::TimeInterval& aInterval); @@ -90,6 +94,7 @@ private: dom::MediaSource* mMediaSource; RefPtr mDemuxer; RefPtr mReader; + MediaEventProducer mWaitingForKeyEvent; bool mEnded; }; diff --git a/dom/media/mediasource/SourceBufferResource.cpp b/dom/media/mediasource/SourceBufferResource.cpp index 6992049ba1f7..bb11072aeb5f 100644 --- a/dom/media/mediasource/SourceBufferResource.cpp +++ b/dom/media/mediasource/SourceBufferResource.cpp @@ -35,50 +35,6 @@ SourceBufferResource::Close() return NS_OK; } -nsresult -SourceBufferResource::ReadInternal(char* aBuffer, uint32_t aCount, uint32_t* aBytes, bool aMayBlock) -{ - mMonitor.AssertCurrentThreadIn(); - MOZ_ASSERT_IF(!aMayBlock, aBytes); - - // Cache the offset for the read in case mOffset changes while waiting on the - // monitor below. It's basically impossible to implement these API semantics - // sanely. :-( - uint64_t readOffset = mOffset; - - while (aMayBlock && - !mEnded && - readOffset + aCount > static_cast(GetLength())) { - SBR_DEBUGV("waiting for data"); - mMonitor.Wait(); - // The callers of this function should have checked this, but it's - // possible that we had an eviction while waiting on the monitor. - if (readOffset < mInputBuffer.GetOffset()) { - return NS_ERROR_FAILURE; - } - } - - uint32_t available = GetLength() - readOffset; - uint32_t count = std::min(aCount, available); - SBR_DEBUGV("readOffset=%llu GetLength()=%u available=%u count=%u mEnded=%d", - readOffset, GetLength(), available, count, mEnded); - if (available == 0) { - SBR_DEBUGV("reached EOF"); - *aBytes = 0; - return NS_OK; - } - - mInputBuffer.CopyData(readOffset, count, aBuffer); - *aBytes = count; - - // From IRC: - // <@cpearce>bholley: *this* is why there should only every be a ReadAt() and - // no Read() on a Stream abstraction! there's no good answer, they all suck. - mOffset = readOffset + count; - - return NS_OK; -} - nsresult SourceBufferResource::ReadAt(int64_t aOffset, char* aBuffer, uint32_t aCount, uint32_t* aBytes) { @@ -93,18 +49,8 @@ SourceBufferResource::ReadAtInternal(int64_t aOffset, char* aBuffer, uint32_t aC bool aMayBlock) { mMonitor.AssertCurrentThreadIn(); - nsresult rv = SeekInternal(aOffset); - if (NS_FAILED(rv)) { - return rv; - } - return ReadInternal(aBuffer, aCount, aBytes, aMayBlock); -} - -nsresult -SourceBufferResource::SeekInternal(int64_t aOffset) -{ - mMonitor.AssertCurrentThreadIn(); + MOZ_ASSERT_IF(!aMayBlock, aBytes); if (mClosed || aOffset < 0 || @@ -113,7 +59,36 @@ SourceBufferResource::SeekInternal(int64_t aOffset) return NS_ERROR_FAILURE; } - mOffset = aOffset; + while (aMayBlock && + !mEnded && + aOffset + aCount > GetLength()) { + SBR_DEBUGV("waiting for data"); + mMonitor.Wait(); + // The callers of this function should have checked this, but it's + // possible that we had an eviction while waiting on the monitor. + if (uint64_t(aOffset) < mInputBuffer.GetOffset()) { + return NS_ERROR_FAILURE; + } + } + + uint32_t available = GetLength() - aOffset; + uint32_t count = std::min(aCount, available); + + // Keep the position of the last read to have Tell() approximately give us + // the position we're up to in the stream. + mOffset = aOffset + count; + + SBR_DEBUGV("offset=%llu GetLength()=%u available=%u count=%u mEnded=%d", + aOffset, GetLength(), available, count, mEnded); + if (available == 0) { + SBR_DEBUGV("reached EOF"); + *aBytes = 0; + return NS_OK; + } + + mInputBuffer.CopyData(aOffset, count, aBuffer); + *aBytes = count; + return NS_OK; } @@ -124,9 +99,7 @@ SourceBufferResource::ReadFromCache(char* aBuffer, int64_t aOffset, uint32_t aCo aBuffer, aOffset, aCount); ReentrantMonitorAutoEnter mon(mMonitor); uint32_t bytesRead; - int64_t oldOffset = mOffset; nsresult rv = ReadAtInternal(aOffset, aBuffer, aCount, &bytesRead, /* aMayBlock = */ false); - mOffset = oldOffset; // ReadFromCache isn't supposed to affect the seek position. NS_ENSURE_SUCCESS(rv, rv); // ReadFromCache return failure if not all the data is cached. @@ -154,10 +127,9 @@ SourceBufferResource::EvictBefore(uint64_t aOffset, ErrorResult& aRv) { SBR_DEBUG("EvictBefore(aOffset=%llu)", aOffset); ReentrantMonitorAutoEnter mon(mMonitor); - // If aOffset is past the current playback offset we don't evict. - if (aOffset < mOffset) { - mInputBuffer.EvictBefore(aOffset, aRv); - } + + mInputBuffer.EvictBefore(aOffset, aRv); + // Wake up any waiting threads in case a ReadInternal call // is now invalid. mon.NotifyAll(); diff --git a/dom/media/mediasource/SourceBufferResource.h b/dom/media/mediasource/SourceBufferResource.h index ddf20b33e5c6..26ed74602272 100644 --- a/dom/media/mediasource/SourceBufferResource.h +++ b/dom/media/mediasource/SourceBufferResource.h @@ -136,8 +136,6 @@ public: private: virtual ~SourceBufferResource(); - nsresult SeekInternal(int64_t aOffset); - nsresult ReadInternal(char* aBuffer, uint32_t aCount, uint32_t* aBytes, bool aMayBlock); nsresult ReadAtInternal(int64_t aOffset, char* aBuffer, uint32_t aCount, uint32_t* aBytes, bool aMayBlock); const nsCString mType; diff --git a/dom/media/platforms/PlatformDecoderModule.h b/dom/media/platforms/PlatformDecoderModule.h index 2d38d8b0a0b7..76577ab51eed 100644 --- a/dom/media/platforms/PlatformDecoderModule.h +++ b/dom/media/platforms/PlatformDecoderModule.h @@ -179,9 +179,15 @@ public: virtual void DrainComplete() = 0; - virtual void ReleaseMediaResources() {}; + virtual void ReleaseMediaResources() {} virtual bool OnReaderTaskQueue() = 0; + + // Denotes that a pending encryption key is preventing more input being fed + // into the decoder. This only needs to be overridden for callbacks that + // handle encryption. E.g. benchmarking does not use eme, so this need + // not be overridden in that case. + virtual void WaitingForKey() {} }; // MediaDataDecoder is the interface exposed by decoders created by the diff --git a/dom/media/platforms/agnostic/eme/EMEDecoderModule.cpp b/dom/media/platforms/agnostic/eme/EMEDecoderModule.cpp index 0ef11e106a2a..d9db494e6e7b 100644 --- a/dom/media/platforms/agnostic/eme/EMEDecoderModule.cpp +++ b/dom/media/platforms/agnostic/eme/EMEDecoderModule.cpp @@ -34,7 +34,8 @@ public: , mCallback(aCallback) , mTaskQueue(aDecodeTaskQueue) , mProxy(aProxy) - , mSamplesWaitingForKey(new SamplesWaitingForKey(this, mTaskQueue, mProxy)) + , mSamplesWaitingForKey(new SamplesWaitingForKey(this, this->mCallback, + mTaskQueue, mProxy)) , mIsShutdown(false) { } @@ -171,7 +172,8 @@ public: CDMProxy* aProxy, TaskQueue* aTaskQueue) : MediaDataDecoderProxy(Move(aProxyThread), aCallback) - , mSamplesWaitingForKey(new SamplesWaitingForKey(this, aTaskQueue, aProxy)) + , mSamplesWaitingForKey(new SamplesWaitingForKey(this, aCallback, + aTaskQueue, aProxy)) , mProxy(aProxy) { } diff --git a/dom/media/platforms/agnostic/eme/SamplesWaitingForKey.cpp b/dom/media/platforms/agnostic/eme/SamplesWaitingForKey.cpp index 1b0fba23b0a0..58098c2b39a2 100644 --- a/dom/media/platforms/agnostic/eme/SamplesWaitingForKey.cpp +++ b/dom/media/platforms/agnostic/eme/SamplesWaitingForKey.cpp @@ -12,10 +12,12 @@ namespace mozilla { SamplesWaitingForKey::SamplesWaitingForKey(MediaDataDecoder* aDecoder, + MediaDataDecoderCallback* aCallback, TaskQueue* aTaskQueue, CDMProxy* aProxy) : mMutex("SamplesWaitingForKey") , mDecoder(aDecoder) + , mDecoderCallback(aCallback) , mTaskQueue(aTaskQueue) , mProxy(aProxy) { @@ -38,6 +40,7 @@ SamplesWaitingForKey::WaitIfKeyNotUsable(MediaRawData* aSample) MutexAutoLock lock(mMutex); mSamples.AppendElement(aSample); } + mDecoderCallback->WaitingForKey(); caps.NotifyWhenKeyIdUsable(aSample->mCrypto.mKeyId, this); return true; } diff --git a/dom/media/platforms/agnostic/eme/SamplesWaitingForKey.h b/dom/media/platforms/agnostic/eme/SamplesWaitingForKey.h index a763180e6e06..65bb14403070 100644 --- a/dom/media/platforms/agnostic/eme/SamplesWaitingForKey.h +++ b/dom/media/platforms/agnostic/eme/SamplesWaitingForKey.h @@ -18,13 +18,14 @@ typedef nsTArray CencKeyId; class CDMProxy; // Encapsulates the task of waiting for the CDMProxy to have the necessary -// keys to decypt a given sample. +// keys to decrypt a given sample. class SamplesWaitingForKey { public: NS_INLINE_DECL_THREADSAFE_REFCOUNTING(SamplesWaitingForKey) explicit SamplesWaitingForKey(MediaDataDecoder* aDecoder, + MediaDataDecoderCallback* aCallback, TaskQueue* aTaskQueue, CDMProxy* aProxy); @@ -46,6 +47,7 @@ protected: private: Mutex mMutex; RefPtr mDecoder; + MediaDataDecoderCallback* mDecoderCallback; RefPtr mTaskQueue; RefPtr mProxy; nsTArray> mSamples; diff --git a/dom/media/platforms/agnostic/gmp/MediaDataDecoderProxy.h b/dom/media/platforms/agnostic/gmp/MediaDataDecoderProxy.h index 45ad6a95ec85..8aa1252f12aa 100644 --- a/dom/media/platforms/agnostic/gmp/MediaDataDecoderProxy.h +++ b/dom/media/platforms/agnostic/gmp/MediaDataDecoderProxy.h @@ -95,6 +95,11 @@ public: return mProxyCallback->OnReaderTaskQueue(); } + void WaitingForKey() override + { + mProxyCallback->WaitingForKey(); + } + private: MediaDataDecoderProxy* mProxyDecoder; MediaDataDecoderCallback* mProxyCallback; diff --git a/dom/media/test/background_video.js b/dom/media/test/background_video.js index 921b9334bc07..62c457cb8e8c 100644 --- a/dom/media/test/background_video.js +++ b/dom/media/test/background_video.js @@ -18,10 +18,16 @@ function startTest(test) { * @param {string} url video src. * @returns {HTMLMediaElement} The created video element. */ -function appendVideoToDoc(url, token) { +function appendVideoToDoc(url, token, width, height) { + // Default size of (160, 120) is used by other media tests. + if (width === undefined) { width = 160; } + if (height === undefined) { height = 3*width/4; } + let v = document.createElement('video'); v.token = token; document.body.appendChild(v); + v.width = width; + v.height = height; v.src = url; return v; } @@ -106,4 +112,4 @@ function waitTil(video, time) { } }); }); -} \ No newline at end of file +} diff --git a/dom/media/test/eme.js b/dom/media/test/eme.js index 1ea3f06a358c..ec49c1511744 100644 --- a/dom/media/test/eme.js +++ b/dom/media/test/eme.js @@ -249,7 +249,7 @@ function LoadTest(test, elem, token, loadParams) Log(token, "sourceopen"); return Promise.all(test.tracks.map(function(track) { return AppendTrack(test, ms, track, token, loadParams); - })).then(function(){ + })).then(function() { if (loadParams && loadParams.noEndOfStream) { Log(token, "Tracks loaded"); } else { @@ -257,6 +257,8 @@ function LoadTest(test, elem, token, loadParams) ms.endOfStream(); } resolve(); + }).catch(function() { + Log(token, "error while loading tracks"); }); }) }); @@ -299,7 +301,7 @@ function SetupEME(test, token, params) // Log events dispatched to make debugging easier... [ "canplay", "canplaythrough", "ended", "error", "loadeddata", "loadedmetadata", "loadstart", "pause", "play", "playing", "progress", - "stalled", "suspend", "waiting", + "stalled", "suspend", "waiting", "waitingforkey", ].forEach(function (e) { v.addEventListener(e, function(event) { Log(token, "" + e); @@ -314,13 +316,28 @@ function SetupEME(test, token, params) : bail(token + " Failed to set MediaKeys on