Merge autoland to mozilla-central. a=merge

This commit is contained in:
Csoregi Natalia 2018-10-12 19:55:37 +03:00
commit 86dc471972
92 changed files with 1590 additions and 913 deletions

View File

@ -165,15 +165,17 @@ DistributionCustomizer.prototype = {
break;
case "livemark":
if (itemIndex < defaultIndex)
// Livemarks are no more supported, instead of a livemark we'll insert
// a bookmark pointing to the site uri, if available.
if (!item.siteLink) {
break;
}
if (itemIndex < defaultIndex) {
index = prependIndex++;
}
// Don't bother updating the livemark contents on creation.
let parentId = await PlacesUtils.promiseItemId(parentGuid);
await PlacesUtils.livemarks.addLivemark({
feedURI: Services.io.newURI(item.feedLink),
siteURI: Services.io.newURI(item.siteLink),
parentId, index, title: item.title,
await PlacesUtils.bookmarks.insert({
parentGuid, index, title: item.title, url: item.siteLink,
});
break;

View File

@ -203,8 +203,16 @@ class ExtensionControlledPopup {
return;
}
// Find the elements we need.
let win = targetWindow || this.topWindow;
// If the window closes while waiting for focus, this might reject/throw,
// and we should stop trying to show the popup.
try {
await this._ensureWindowReady(win);
} catch (ex) {
return;
}
// Find the elements we need.
let doc = win.document;
let panel = doc.getElementById("extension-notification-panel");
let popupnotification = doc.getElementById(this.popupnotificationId);
@ -301,4 +309,53 @@ class ExtensionControlledPopup {
link.textContent = strBundle.GetStringFromName(this.learnMoreMessageId);
description.appendChild(link);
}
_ensureWindowReady(win) {
return new Promise(async (resolve, reject) => {
if (win.closed) {
reject();
return;
}
let promises = [];
let listenersToRemove = [];
function promiseEvent(type) {
promises.push(new Promise(resolve => {
let listener = () => {
win.removeEventListener(type, listener);
resolve();
};
win.addEventListener(type, listener);
listenersToRemove.push([type, listener]);
}));
}
let {focusedWindow, activeWindow} = Services.focus;
if (activeWindow != win) {
promiseEvent("activate");
}
if (focusedWindow) {
// We may have focused a non-remote child window, find the browser window:
let {rootTreeItem} = focusedWindow.docShell;
rootTreeItem.QueryInterface(Ci.nsIDocShell);
focusedWindow = rootTreeItem.contentViewer.DOMDocument.defaultView;
}
if (focusedWindow != win) {
promiseEvent("focus");
}
let unloadListener;
if (promises.length) {
unloadListener = () => {
for (let [type, listener] of listenersToRemove) {
win.removeEventListener(type, listener);
}
reject();
};
win.addEventListener("unload", unloadListener, {once: true});
}
await Promise.all(promises);
if (unloadListener) {
win.removeEventListener("unload", unloadListener);
}
resolve();
});
}
}

View File

@ -2,9 +2,8 @@
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
XPCOMUtils.defineLazyServiceGetter(this, "aboutNewTabService",
"@mozilla.org/browser/aboutnewtab-service;1",
"nsIAboutNewTabService");
ChromeUtils.defineModuleGetter(this, "HomePage",
"resource:///modules/HomePage.jsm");
ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
@ -152,9 +151,6 @@ this.windows = class extends ExtensionAPI {
if (createData.incognito !== null && createData.incognito != incognito) {
return Promise.reject({message: "`incognito` property must match the incognito state of tab"});
}
if (createData.incognito && !PrivateBrowsingUtils.enabled) {
return Promise.reject({message: "`incognito` cannot be used if incognito mode is disabled"});
}
createData.incognito = incognito;
if (createData.cookieStoreId && createData.cookieStoreId !== getCookieStoreIdForTab(createData, tab)) {
@ -173,12 +169,14 @@ this.windows = class extends ExtensionAPI {
args.appendElement(mkstr(createData.url));
}
} else {
let url = aboutNewTabService.newTabURL;
let url = createData.incognito && !PrivateBrowsingUtils.permanentPrivateBrowsing ?
"about:privatebrowsing" : HomePage.get().split("|", 1)[0];
args.appendElement(mkstr(url));
if (url === "about:newtab") {
// The extension principal cannot directly load about:newtab,
// so use the system principal instead.
if (url.startsWith("about:") &&
!context.checkLoadURL(url, {dontReportErrors: true})) {
// The extension principal cannot directly load about:-URLs,
// except for about:blank. So use the system principal instead.
principal = Services.scriptSecurityManager.getSystemPrincipal();
}
}
@ -213,6 +211,9 @@ this.windows = class extends ExtensionAPI {
if (createData.incognito !== null) {
if (createData.incognito) {
if (!PrivateBrowsingUtils.enabled) {
return Promise.reject({message: "`incognito` cannot be used if incognito mode is disabled"});
}
features.push("private");
} else {
features.push("non-private");

View File

@ -2,6 +2,26 @@
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
async function runWithDisabledPrivateBrowsing(callback) {
const {
EnterprisePolicyTesting,
PoliciesPrefTracker,
} = ChromeUtils.import("resource://testing-common/EnterprisePolicyTesting.jsm", {});
PoliciesPrefTracker.start();
await EnterprisePolicyTesting.setupPolicyEngineWithJson({
policies: {DisablePrivateBrowsing: true},
});
try {
await callback();
} finally {
await EnterprisePolicyTesting.setupPolicyEngineWithJson("");
EnterprisePolicyTesting.resetRunOnceState();
PoliciesPrefTracker.stop();
}
}
add_task(async function test_urlbar_focus() {
// Disable preloaded new tab because the urlbar is automatically focused when
// a preloaded new tab is opened, while this test is supposed to test that the
@ -60,3 +80,112 @@ add_task(async function test_urlbar_focus() {
await extension.unload();
});
add_task(async function default_url() {
const extension = ExtensionTestUtils.loadExtension({
manifest: {
permissions: ["tabs"],
},
background() {
function promiseNonBlankTab() {
return new Promise(resolve => {
browser.tabs.onUpdated.addListener(function listener(tabId, changeInfo, tab) {
if (changeInfo.status === "complete" && tab.url !== "about:blank") {
browser.tabs.onUpdated.removeListener(listener);
resolve(tab);
}
});
});
}
browser.test.onMessage.addListener(async (msg, {incognito, expectedNewWindowUrl, expectedNewTabUrl}) => {
browser.test.assertEq("start", msg, `Start test, incognito=${incognito}`);
let tabPromise = promiseNonBlankTab();
let win;
try {
win = await browser.windows.create({incognito});
browser.test.assertEq(1, win.tabs.length, "Expected one tab in the new window.");
} catch (e) {
browser.test.assertEq(expectedNewWindowUrl, e.message, "Expected error");
browser.test.sendMessage("done");
return;
}
let tab = await tabPromise;
browser.test.assertEq(expectedNewWindowUrl, tab.url, "Expected default URL of new window");
tabPromise = promiseNonBlankTab();
await browser.tabs.create({windowId: win.id});
tab = await tabPromise;
browser.test.assertEq(expectedNewTabUrl, tab.url, "Expected default URL of new tab");
await browser.windows.remove(win.id);
browser.test.sendMessage("done");
});
},
});
await extension.startup();
extension.sendMessage("start", {
incognito: false,
expectedNewWindowUrl: "about:home",
expectedNewTabUrl: "about:newtab",
});
await extension.awaitMessage("done");
extension.sendMessage("start", {
incognito: true,
expectedNewWindowUrl: "about:privatebrowsing",
expectedNewTabUrl: "about:privatebrowsing",
});
await extension.awaitMessage("done");
info("Testing with multiple homepages.");
await SpecialPowers.pushPrefEnv({set: [["browser.startup.homepage", "about:robots|about:blank|about:home"]]});
extension.sendMessage("start", {
incognito: false,
expectedNewWindowUrl: "about:robots",
expectedNewTabUrl: "about:newtab",
});
await extension.awaitMessage("done");
extension.sendMessage("start", {
incognito: true,
expectedNewWindowUrl: "about:privatebrowsing",
expectedNewTabUrl: "about:privatebrowsing",
});
await extension.awaitMessage("done");
await SpecialPowers.popPrefEnv();
info("Testing with perma-private browsing mode.");
await SpecialPowers.pushPrefEnv({set: [["browser.privatebrowsing.autostart", true]]});
extension.sendMessage("start", {
incognito: false,
expectedNewWindowUrl: "about:home",
expectedNewTabUrl: "about:newtab",
});
await extension.awaitMessage("done");
extension.sendMessage("start", {
incognito: true,
expectedNewWindowUrl: "about:home",
expectedNewTabUrl: "about:newtab",
});
await extension.awaitMessage("done");
await SpecialPowers.popPrefEnv();
info("Testing with disabled private browsing mode.");
await runWithDisabledPrivateBrowsing(async () => {
extension.sendMessage("start", {
incognito: false,
expectedNewWindowUrl: "about:home",
expectedNewTabUrl: "about:newtab",
});
await extension.awaitMessage("done");
extension.sendMessage("start", {
incognito: true,
expectedNewWindowUrl: "`incognito` cannot be used if incognito mode is disabled",
});
await extension.awaitMessage("done");
});
await extension.unload();
});

View File

@ -74,7 +74,7 @@ add_task(async function valid_cookieStoreId() {
"firefox-container-1",
],
expectedExecuteScriptResult: [
// Default URL is about:newtab, and extensions cannot run scripts in it.
// Default URL is about:home, and extensions cannot run scripts in it.
"Missing host permission for the tab",
],
}, {
@ -169,7 +169,7 @@ add_task(async function valid_cookieStoreId() {
for (let [i, expectedResult] of Object.entries(expectedExecuteScriptResult)) {
// Wait until the the tab can process the tabs.executeScript calls.
// TODO: Remove this when bug 1418655 and bug 1397667 are fixed.
let expectedUrl = Array.isArray(createParams.url) ? createParams.url[i] : createParams.url || "about:newtab";
let expectedUrl = Array.isArray(createParams.url) ? createParams.url[i] : createParams.url || "about:home";
await awaitTabReady(win.tabs[i].id, expectedUrl);
let result = await executeScriptAndGetResult(win.tabs[i].id);

View File

@ -53,7 +53,16 @@ function installDistributionEngine() {
});
}
function run_test() {
registerCleanupFunction(async function() {
// Remove the distribution dir, even if the test failed, otherwise all
// next tests will use it.
let folderPath = OS.Path.join(OS.Constants.Path.profileDir, "distribution");
await OS.File.removeDir(folderPath, { ignoreAbsent: true });
Assert.ok(!(await OS.File.exists(folderPath)));
Services.prefs.clearUserPref("distribution.testing.loadFromProfile");
});
add_task(async function() {
// Set special pref to load distribution.ini from the profile folder.
Services.prefs.setBoolPref("distribution.testing.loadFromProfile", true);
@ -73,18 +82,6 @@ function run_test() {
Assert.ok(testDistributionFile.exists());
installDistributionEngine();
run_next_test();
}
registerCleanupFunction(function() {
// Remove the distribution dir, even if the test failed, otherwise all
// next tests will use it.
let distDir = gProfD.clone();
distDir.append("distribution");
distDir.remove(true);
Assert.ok(!distDir.exists());
Services.prefs.clearUserPref("distribution.testing.loadFromProfile");
});
add_task(async function() {
@ -147,14 +144,14 @@ add_task(async function() {
// Language should not override locale
Assert.notEqual(defaultBranch.getComplexValue("distribution.test.locale.set", Ci.nsIPrefLocalizedString).data, "Language Set");
do_test_pending();
Services.prefs.setCharPref("distribution.searchplugins.defaultLocale", "de-DE");
Services.search.init(function() {
Assert.equal(Services.search.isInitialized, true);
var engine = Services.search.getEngineByName("Google");
Assert.equal(engine.description, "override-de-DE");
do_test_finished();
await new Promise(resolve => {
Services.search.init(function() {
Assert.equal(Services.search.isInitialized, true);
var engine = Services.search.getEngineByName("Google");
Assert.equal(engine.description, "override-de-DE");
resolve();
});
});
});

View File

@ -31,8 +31,8 @@ add_task(async function() {
});
function testAccordionStateAfterClickingHeader(doc) {
const header = doc.querySelector("#layout-container .box-model-pane ._header");
const bContent = doc.querySelector("#layout-container .box-model-pane ._content");
const header = doc.querySelector(".layout-container .box-model-pane ._header");
const bContent = doc.querySelector(".layout-container .box-model-pane ._content");
info("Checking initial state of the box model panel.");
is(bContent.style.display, "block", "The box model panel content is 'display: block'.");
@ -51,7 +51,7 @@ function testAccordionStateAfterClickingHeader(doc) {
function testAccordionStateAfterSwitchingSidebars(inspector, doc) {
info("Checking the box model accordion state is persistent after switching sidebars.");
const bContent = doc.querySelector("#layout-container .box-model-pane ._content");
const bContent = doc.querySelector(".layout-container .box-model-pane ._content");
info("Selecting the computed view.");
inspector.sidebar.select("computedview");
@ -75,7 +75,7 @@ async function testAccordionStateAfterReopeningLayoutView(toolbox) {
info("Re-opening the layout view.");
const { boxmodel } = await openLayoutView();
const { document: doc } = boxmodel;
const bContent = doc.querySelector("#layout-container .box-model-pane ._content");
const bContent = doc.querySelector(".layout-container .box-model-pane ._content");
info("Checking the state of the box model panel.");
ok(!bContent, "The box model panel content is not rendered.");

View File

@ -46,10 +46,7 @@ class FlexItemSizingOutline extends PureComponent {
);
}
renderFinalOutline(mainFinalSize, mainMaxSize, mainMinSize) {
const isClamped = mainFinalSize === mainMaxSize ||
mainFinalSize === mainMinSize;
renderFinalOutline(mainFinalSize, mainMaxSize, mainMinSize, isClamped) {
return (
dom.div({
className: "flex-outline-final" + (isClamped ? " clamped" : "")
@ -62,13 +59,19 @@ class FlexItemSizingOutline extends PureComponent {
}
render() {
const {
flexItemSizing,
properties,
} = this.props.flexItem;
const {
mainBaseSize,
mainDeltaSize,
mainMaxSize,
mainMinSize,
} = this.props.flexItem.flexItemSizing;
} = flexItemSizing;
const isRow = this.props.flexDirection.startsWith("row");
const dimension = isRow ? "width" : "height";
// Calculate the final size. This is base + delta, then clamped by min or max.
let mainFinalSize = mainBaseSize + mainDeltaSize;
@ -76,10 +79,17 @@ class FlexItemSizingOutline extends PureComponent {
mainFinalSize = Math.min(mainFinalSize, mainMaxSize);
// The max size is only interesting to show if it did clamp the item
// TODO: replace this with the new clamping state that the API will return once bug
// 1498273 is fixed.
const showMax = mainMaxSize === mainFinalSize;
// The min size is only really interesting if it actually clamped the item.
const showMin = mainMinSize === mainFinalSize;
// Just checking that the main size = final size isn't enough because this may be true
// if the max content size is the final size. So also check that min-width/height is
// set.
// TODO: replace this with the new clamping state that the API will return once bug
// 1498273 is fixed.
const showMin = mainMinSize === mainFinalSize && properties[`min-${dimension}`];
// Sort all of the dimensions in order to come up with a grid track template.
// Make mainDeltaSize start from the same point as the other ones so we can compare.
@ -137,7 +147,8 @@ class FlexItemSizingOutline extends PureComponent {
showMax ? this.renderPoint("max") : null,
this.renderBasisOutline(mainBaseSize),
this.renderDeltaOutline(mainDeltaSize),
this.renderFinalOutline(mainFinalSize, mainMaxSize, mainMinSize)
this.renderFinalOutline(mainFinalSize, mainMaxSize, mainMinSize,
showMin || showMax)
)
)
);

View File

@ -19,6 +19,300 @@ class FlexItemSizingProperties extends PureComponent {
};
}
/**
* Rounds some dimension in pixels and returns a string to be displayed to the user.
* The string will end with 'px'. If the number is 0, the string "0" is returned.
*
* @param {Number} value
* The number to be rounded
* @return {String}
* Representation of the rounded number
*/
getRoundedDimension(value) {
if (value == 0) {
return "0";
}
return (Math.round(value * 100) / 100) + "px";
}
/**
* Format the flexibility value into a meaningful value for the UI.
* If the item grew, then prepend a + sign, if it shrank, prepend a - sign.
* If it didn't flex, return "0".
*
* @param {Boolean} grew
* Whether the item grew or not
* @param {Number} value
* The amount of pixels the item flexed
* @return {String}
* Representation of the flexibility value
*/
getFlexibilityValueString(grew, mainDeltaSize) {
const value = this.getRoundedDimension(mainDeltaSize);
if (grew) {
return "+" + value;
}
return value;
}
/**
* Render an authored CSS property.
*
* @param {String} name
* The name for this CSS property
* @param {String} value
* The property value
* @param {Booleam} isDefaultValue
* Whether the value come from the browser default style
* @return {Object}
* The React component representing this CSS property
*/
renderCssProperty(name, value, isDefaultValue) {
return (
dom.span({ className: "css-property-link" },
dom.span({ className: "theme-fg-color5" }, name),
": ",
dom.span({ className: "theme-fg-color1" }, value),
";"
)
);
}
/**
* Render a list of sentences to be displayed in the UI as reasons why a certain sizing
* value happened.
*
* @param {Array} sentences
* The list of sentences as Strings
* @return {Object}
* The React component representing these sentences
*/
renderReasons(sentences) {
return (
dom.ul({ className: "reasons" },
sentences.map(sentence => dom.li({}, sentence))
)
);
}
renderBaseSizeSection({ mainBaseSize, mainMinSize }, properties, dimension) {
const flexBasisValue = properties["flex-basis"];
const dimensionValue = properties[dimension];
const minDimensionValue = properties[`min-${dimension}`];
const hasMinClamping = mainMinSize && mainMinSize === mainBaseSize;
let property = null;
let reason = null;
if (hasMinClamping && minDimensionValue) {
// If min clamping happened, then the base size is going to be that value.
// TODO: this isn't going to be necessarily true after bug 1498273 is fixed.
property = this.renderCssProperty(`min-${dimension}`, minDimensionValue);
} else if (flexBasisValue && !hasMinClamping) {
// If flex-basis is defined, then that's what is used for the base size.
property = this.renderCssProperty("flex-basis", flexBasisValue);
} else if (dimensionValue) {
// If not and width/height is defined, then that's what defines the base size.
property = this.renderCssProperty(dimension, dimensionValue);
} else {
// Finally, if nothing is set, then the base size is the max-content size.
reason = this.renderReasons(
[getStr("flexbox.itemSizing.itemBaseSizeFromContent")]);
}
return (
dom.li({ className: property ? "section" : "section no-property" },
dom.span({ className: "name" },
getStr("flexbox.itemSizing.baseSizeSectionHeader")
),
dom.span({ className: "value theme-fg-color1" },
this.getRoundedDimension(mainBaseSize)
),
property,
reason
)
);
}
renderFlexibilitySection(flexItemSizing, properties) {
const {
mainDeltaSize,
mainBaseSize,
mainFinalSize,
lineGrowthState
} = flexItemSizing;
const flexGrow = properties["flex-grow"];
const flexGrow0 = parseFloat(flexGrow) === 0;
const flexShrink = properties["flex-shrink"];
const flexShrink0 = parseFloat(flexShrink) === 0;
const grew = mainDeltaSize > 0;
const shrank = mainDeltaSize < 0;
// TODO: replace this with the new clamping state that the API will return once bug
// 1498273 is fixed.
const wasClamped = mainDeltaSize + mainBaseSize !== mainFinalSize;
const reasons = [];
// First output a sentence for telling users about whether there was enough room or
// not on the line.
if (lineGrowthState === "growing") {
reasons.push(getStr("flexbox.itemSizing.extraRoomOnLine"));
} else if (lineGrowthState === "shrinking") {
reasons.push(getStr("flexbox.itemSizing.notEnoughRoomOnLine"));
}
// Then tell users whether the item was set to grow, shrink or none of them.
if (flexGrow && !flexGrow0 && lineGrowthState !== "shrinking") {
reasons.push(getStr("flexbox.itemSizing.setToGrow"));
}
if (flexShrink && !flexShrink0 && lineGrowthState !== "growing") {
reasons.push(getStr("flexbox.itemSizing.setToShrink"));
}
if (!grew && !shrank && lineGrowthState === "growing") {
reasons.push(getStr("flexbox.itemSizing.notSetToGrow"));
}
if (!grew && !shrank && lineGrowthState === "shrinking") {
reasons.push(getStr("flexbox.itemSizing.notSetToShrink"));
}
let property = null;
if (grew) {
// If the item grew.
if (flexGrow) {
// It's normally because it was set to grow (flex-grow is non 0).
property = this.renderCssProperty("flex-grow", flexGrow);
}
if (wasClamped) {
// It may have wanted to grow more than it did, because it was later max-clamped.
reasons.push(getStr("flexbox.itemSizing.growthAttemptWhenClamped"));
}
} else if (shrank) {
// If the item shrank.
if (flexShrink && !flexShrink0) {
// It's either because flex-shrink is non 0.
property = this.renderCssProperty("flex-shrink", flexShrink);
} else {
// Or also because it's default value is 1 anyway.
property = this.renderCssProperty("flex-shrink", "1", true);
}
if (wasClamped) {
// It might have wanted to shrink more (to accomodate all items) but couldn't
// because it was later min-clamped.
reasons.push(getStr("flexbox.itemSizing.shrinkAttemptWhenClamped"));
}
} else if (lineGrowthState === "growing" && flexGrow && !flexGrow0) {
// The item did not grow or shrink. There was room on the line and flex-grow was
// set, other items have likely used up all of the space.
property = this.renderCssProperty("flex-grow", flexGrow);
reasons.push(getStr("flexbox.itemSizing.growthAttemptButSiblings"));
} else if (lineGrowthState === "shrinking") {
// The item did not grow or shrink and there wasn't enough room on the line.
if (!flexShrink0) {
// flex-shrink was set (either defined in CSS, or via its default value of 1).
// but the item didn't shrink.
if (flexShrink) {
property = this.renderCssProperty("flex-shrink", flexShrink);
} else {
property = this.renderCssProperty("flex-shrink", 1, true);
}
reasons.push(getStr("flexbox.itemSizing.shrinkAttemptButCouldnt"));
if (wasClamped) {
// Maybe it was clamped.
reasons.push(getStr("flexbox.itemSizing.shrinkAttemptWhenClamped"));
}
} else {
// flex-shrink was set to 0, so it didn't shrink.
property = this.renderCssProperty("flex-shrink", flexShrink);
}
}
// Don't display the section at all if there's nothing useful to show users.
if (!property && !reasons.length) {
return null;
}
return (
dom.li({ className: property ? "section" : "section no-property" },
dom.span({ className: "name" },
getStr("flexbox.itemSizing.flexibilitySectionHeader")
),
dom.span({ className: "value theme-fg-color1" },
this.getFlexibilityValueString(grew, mainDeltaSize)
),
property,
this.renderReasons(reasons)
)
);
}
renderMinimumSizeSection({ mainMinSize, mainFinalSize }, properties, dimension) {
// We only display the minimum size when the item actually violates that size during
// layout & is clamped.
// For now, we detect this by checking that the min-size is the same as the final size
// and that a min-size is actually defined in CSS.
// TODO: replace this with the new clamping state that the API will return once bug
// 1498273 is fixed.
const minDimensionValue = properties[`min-${dimension}`];
if (mainMinSize !== mainFinalSize || !minDimensionValue) {
return null;
}
return (
dom.li({ className: "section" },
dom.span({ className: "name" },
getStr("flexbox.itemSizing.minSizeSectionHeader")
),
dom.span({ className: "value theme-fg-color1" },
this.getRoundedDimension(mainMinSize)
),
this.renderCssProperty(`min-${dimension}`, minDimensionValue)
)
);
}
renderMaximumSizeSection({ mainMaxSize, mainFinalSize }, properties, dimension) {
// TODO: replace this with the new clamping state that the API will return once bug
// 1498273 is fixed.
if (mainMaxSize !== mainFinalSize) {
return null;
}
const maxDimensionValue = properties[`max-${dimension}`];
return (
dom.li({ className: "section" },
dom.span({ className: "name" },
getStr("flexbox.itemSizing.maxSizeSectionHeader")
),
dom.span({ className: "value theme-fg-color1" },
this.getRoundedDimension(mainMaxSize)
),
this.renderCssProperty(`max-${dimension}`, maxDimensionValue)
)
);
}
renderFinalSizeSection({ mainFinalSize }) {
return (
dom.li({ className: "section no-property" },
dom.span({ className: "name" },
getStr("flexbox.itemSizing.finalSizeSectionHeader")
),
dom.span({ className: "value theme-fg-color1" },
this.getRoundedDimension(mainFinalSize)
)
)
);
}
render() {
const {
flexDirection,
@ -28,46 +322,27 @@ class FlexItemSizingProperties extends PureComponent {
flexItemSizing,
properties,
} = flexItem;
const {
mainBaseSize,
mainDeltaSize,
mainMaxSize,
mainMinSize,
} = flexItemSizing;
const dimension = flexDirection.startsWith("row") ? "width" : "height";
const contentStr = dimension === "width" ?
getStr("flexbox.contentWidth") : getStr("flexbox.contentHeight");
const finalStr = dimension === "width" ?
getStr("flexbox.finalWidth") : getStr("flexbox.finalHeight");
// Calculate the final size. This is base + delta, then clamped by min or max.
let mainFinalSize = mainBaseSize + mainDeltaSize;
mainFinalSize = Math.max(mainFinalSize, mainMinSize);
mainFinalSize = Math.min(mainFinalSize, mainMaxSize);
flexItemSizing.mainFinalSize = mainFinalSize;
return (
dom.ol(
{
id: "flex-item-sizing-properties",
className: "flex-item-list",
},
dom.li({},
dom.span({}, "flex-basis: "),
properties["flex-basis"]
),
dom.li({},
dom.span({}, "flex-grow: "),
properties["flex-grow"]
),
dom.li({},
dom.span({}, "flex-shrink: "),
properties["flex-shrink"]
),
dom.li({},
dom.span({}, `${contentStr} `),
`${parseFloat(flexItemSizing.mainBaseSize.toPrecision(6))}px`
),
dom.li({},
dom.span({}, `Min-${dimension}: `),
properties["min-" + dimension]
),
dom.li({},
dom.span({}, `Max-${dimension}: `),
properties["max-" + dimension]
),
dom.li({},
dom.span({}, `${finalStr} `),
`${parseFloat(properties[dimension].toPrecision(6))}px`
)
dom.ul({ className: "flex-item-sizing" },
this.renderBaseSizeSection(flexItemSizing, properties, dimension),
this.renderFlexibilitySection(flexItemSizing, properties),
this.renderMinimumSizeSection(flexItemSizing, properties, dimension),
this.renderMaximumSizeSection(flexItemSizing, properties, dimension),
this.renderFinalSizeSection(flexItemSizing)
)
);
}

View File

@ -9,5 +9,7 @@ support-files =
!/devtools/client/shared/test/shared-head.js
[browser_flexbox_item_outline_exists.js]
[browser_flexbox_item_outline_rotates_for_column.js]
[browser_flexbox_item_outline_has_correct_layout.js]
[browser_flexbox_item_outline_rotates_for_column.js]
[browser_flexbox_sizing_info_exists.js]
[browser_flexbox_sizing_info_has_correct_sections.js]

View File

@ -0,0 +1,32 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that the flex item sizing information exists when a flex item is selected.
const TEST_URI = URL_ROOT + "doc_flexbox_simple.html";
add_task(async function() {
await addTab(TEST_URI);
const { inspector, flexboxInspector } = await openLayoutView();
const { document: doc } = flexboxInspector;
// Select a flex item in the test document and wait for the sizing info to be rendered.
// Note that we select an item that has base, delta and final sizes, so we can check
// those sections exists.
const onFlexItemSizingRendered = waitForDOM(doc, "ul.flex-item-sizing");
await selectNode(".container.growing .item", inspector);
const [flexSizingContainer] = await onFlexItemSizingRendered;
ok(flexSizingContainer, "The flex sizing exists in the DOM");
info("Check that the base, flexibility and final sizes are displayed");
const allSections = [...flexSizingContainer.querySelectorAll(".section .name")];
const allSectionTitles = allSections.map(el => el.textContent);
const expectedTitles = ["Base Size", "Flexibility", "Final Size"];
ok(expectedTitles.every(title => allSectionTitles.includes(title)),
"The 3 main sizing sections where found");
});

View File

@ -0,0 +1,53 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that the flex item sizing UI contains the right sections, depending on which
// element is selected. Some items may be clamped, others not, so not all sections are
// visible at all times.
const TEST_URI = URL_ROOT + "doc_flexbox_simple.html";
const TEST_DATA = [{
selector: ".shrinking .item",
expectedSections: ["Base Size", "Flexibility", "Final Size"]
}, {
selector: ".shrinking.is-clamped .item",
expectedSections: ["Base Size", "Flexibility", "Minimum Size", "Final Size"]
}, {
selector: ".growing .item",
expectedSections: ["Base Size", "Flexibility", "Final Size"]
}, {
selector: ".growing.is-clamped .item",
expectedSections: ["Base Size", "Flexibility", "Maximum Size", "Final Size"]
}];
add_task(async function() {
await addTab(TEST_URI);
const { inspector, flexboxInspector } = await openLayoutView();
const { document: doc } = flexboxInspector;
for (const { selector, expectedSections } of TEST_DATA) {
info(`Checking the list of sections for the flex item ${selector}`);
const sections = await selectNodeAndGetFlexSizingSections(selector, inspector, doc);
is(sections.length, expectedSections.length, "Correct number of sections found");
expectedSections.forEach((expectedSection, i) => {
is(sections[i], expectedSection, `The ${expectedSection} section was found`);
});
}
});
async function selectNodeAndGetFlexSizingSections(selector, inspector, doc) {
const onFlexItemSizingRendered = waitForDOM(doc, "ul.flex-item-sizing");
await selectNode(selector, inspector);
const [flexSizingContainer] = await onFlexItemSizingRendered;
info(`Getting the list of displayed sections for ${selector}`);
const allSections = [...flexSizingContainer.querySelectorAll(".section .name")];
const allSectionTitles = allSections.map(el => el.textContent);
return allSectionTitles;
}

View File

@ -139,7 +139,7 @@ class LayoutApp extends PureComponent {
}
return (
dom.div({ id: "layout-container" },
dom.div({ className: "layout-container" },
Accordion({ items })
)
);

View File

@ -32,6 +32,87 @@ flexbox.contentHeight=Content height:
flexbox.finalWidth=Final width:
flexbox.finalHeight=Final height:
# LOCALIZATION NOTE (flexbox.itemSizing.baseSizeSectionHeader): Header label displayed
# at the start of the flex item sizing Base Size section.
flexbox.itemSizing.baseSizeSectionHeader=Base Size
# LOCALIZATION NOTE (flexbox.itemSizing.flexibilitySectionHeader): Header label displayed
# at the start of the flex item sizing Flexibility section.
flexbox.itemSizing.flexibilitySectionHeader=Flexibility
# LOCALIZATION NOTE (flexbox.itemSizing.minSizeSectionHeader): Header label displayed
# at the start of the flex item sizing Minimum Size section.
flexbox.itemSizing.minSizeSectionHeader=Minimum Size
# LOCALIZATION NOTE (flexbox.itemSizing.maxSizeSectionHeader): Header label displayed at
# the start of the flex item sizing Maximum Size section.
flexbox.itemSizing.maxSizeSectionHeader=Maximum Size
# LOCALIZATION NOTE (flexbox.itemSizing.finalSizeSectionHeader): Header label displayed at
# the start of the flex item sizing Final Size section.
flexbox.itemSizing.finalSizeSectionHeader=Final Size
# LOCALIZATION NOTE (flexbox.itemSizing.itemBaseSizeFromContent): Label shown in the flex
# item sizing panel. It tells users that a given items base size was calculated from its
# content size when unconstrained.
flexbox.itemSizing.itemBaseSizeFromContent=The items content size when unconstrained.
# LOCALIZATION NOTE (flexbox.itemSizing.itemMinSizeFromItemMinContent): Label shown in the
# flex item sizing panel. It tells users that a given items minimum size is coming from
# its min-content size.
flexbox.itemSizing.itemMinSizeFromItemMinContent=This is the elements minimum content size.
# LOCALIZATION NOTE (flexbox.itemSizing.extraRoomOnLine): Label shown in the flexbox item
# sizing panel. It tells users that there was extra room to distribute inside a given flex
# line.
flexbox.itemSizing.extraRoomOnLine=There was extra room available on the flex line.
# LOCALIZATION NOTE (flexbox.itemSizing.notEnoughRoomOnLine): Label shown in the flexbox
# item sizing panel. It tells users that there wasnt enough room inside a given flex line
# for all of its items.
flexbox.itemSizing.notEnoughRoomOnLine=There wasnt enough room available on the flex line.
# LOCALIZATION NOTE (flexbox.itemSizing.growthAttemptWhenClamped): Label shown in the
# flexbox item sizing panel. It tells users that a given item attempted to grow by a
# certain amount but ended up being clamped by a max size.
# (note that clamp is a common word in flexbox terminology. It refers to constraining an
# item's size to some defined min/max-width/height set on the element, even though there
# might have been room for it to grow, or reason for it to shrink more).
flexbox.itemSizing.growthAttemptWhenClamped=The item wanted to grow, but it was clamped.
# LOCALIZATION NOTE (flexbox.itemSizing.shrinkAttemptWhenClamped): Label shown in the
# flexbox item sizing panel. It tells users that a given item attempted to shrink by a
# certain amount but ended up being clamped by a min size.
flexbox.itemSizing.shrinkAttemptWhenClamped=The item wanted to shrink, but it was clamped.
# LOCALIZATION NOTE (flexbox.itemSizing.shrinkAttemptButCouldnt): Label shown in the
# flexbox item sizing panel. It tells users that a given item attempted to shrink by a
# certain amount but could not
flexbox.itemSizing.shrinkAttemptButCouldnt=Item was set to shrink but could not.
# LOCALIZATION NOTE (flexbox.itemSizing.growthAttemptButSiblings): Label shown in the
# flexbox item sizing panel. It tells users that a given item could not grow to occupy
# extra space because its siblings have likely already used it.
flexbox.itemSizing.growthAttemptButSiblings=Item could not grow, siblings have likely used the extra space.
# LOCALIZATION NOTE (flexbox.itemSizing.setToGrow): Label shown in the flex item sizing
# panel. It tells users that a given item was set to grow.
flexbox.itemSizing.setToGrow=Item was set to grow.
# LOCALIZATION NOTE (flexbox.itemSizing.setToShrink): Label shown in the flexbox item
# sizing panel. It tells users that a given item was set to shrink.
flexbox.itemSizing.setToShrink=Item was set to shrink.
# LOCALIZATION NOTE (flexbox.itemSizing.notSetToGrow): Label shown in the
# flexbox item sizing panel. It tells users that a given item was not set to grow, even
# though there might have been space on the flex line for it to grow.
flexbox.itemSizing.notSetToGrow=Item was not set to grow.
# LOCALIZATION NOTE (flexbox.itemSizing.notSetToShrink): Label shown in the
# flexbox item sizing panel. It tells users that a given item did not shrink even though
# there might not have been enough space on the flex line for all items to fit.
flexbox.itemSizing.notSetToShrink=Item was not set to shrink.
# LOCALIZATION NOTE (layout.cannotShowGridOutline, layout.cannotSHowGridOutline.title):
# In the case where the grid outline cannot be effectively displayed.
layout.cannotShowGridOutline=Cannot show outline for this grid

View File

@ -2,7 +2,7 @@
* 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/. */
#layout-container {
.layout-container {
height: 100%;
width: 100%;
overflow-y: auto;
@ -10,7 +10,7 @@
min-width: 200px;
}
#layout-container .accordion ._content {
.layout-container .accordion ._content {
padding: 0;
}
@ -24,17 +24,17 @@
* Common styles for the layout container
*/
#layout-container li {
.layout-container li {
padding: 3px 0;
-moz-user-select: none;
}
#layout-container input {
.layout-container input {
margin-inline-end: 7px;
vertical-align: middle;
}
#layout-container label {
.layout-container label {
margin-inline-start: -3px;
}
@ -363,12 +363,47 @@
* Flex Item Sizing Properties
*/
#flex-item-sizing-properties {
padding-top: 0;
.flex-item-sizing {
margin: 20px;
padding: 0;
list-style: none;
}
#flex-item-sizing-properties span {
font-weight: 600;
.flex-item-sizing .section {
--padding: 10px;
margin-block-start: var(--padding);
padding: var(--padding) 0 0 0;
border-block-start: 1px solid var(--theme-splitter-color);
display: grid;
grid-template-columns: 1fr max-content;
grid-column-gap: var(--padding);
}
.flex-item-sizing .section:first-child {
margin: 0;
}
.flex-item-sizing .name {
font-weight: 600;
grid-column: 1;
}
.flex-item-sizing .value {
text-align: end;
font-weight: 600;
}
.flex-item-sizing .css-property-link {
grid-column: 2;
text-align: end;
}
.flex-item-sizing .reasons,
.flex-item-sizing .reasons li {
grid-column: 1 / 3;
margin: 0;
padding: 0;
list-style: none;
}
/**

View File

@ -12,11 +12,11 @@ const {
gridSpec,
layoutSpec,
} = require("devtools/shared/specs/layout");
const { ELEMENT_NODE } = require("devtools/shared/dom-node-constants");
const { SHOW_ELEMENT } = require("devtools/shared/dom-node-filter-constants");
const { getStringifiableFragments } =
require("devtools/server/actors/utils/css-grid-utils");
loader.lazyRequireGetter(this, "getCSSStyleRules", "devtools/shared/inspector/css-logic", true);
loader.lazyRequireGetter(this, "CssLogic", "devtools/server/actors/inspector/css-logic", true);
loader.lazyRequireGetter(this, "nodeConstants", "devtools/shared/dom-node-constants");
@ -103,10 +103,6 @@ const FlexboxActor = ActorClassWithSpec(flexboxSpec, {
for (const line of flex.getLines()) {
for (const item of line.getItems()) {
if (item.node.nodeType !== ELEMENT_NODE) {
continue;
}
flexItemActors.push(new FlexItemActor(this, item.node, {
crossMaxSize: item.crossMaxSize,
crossMinSize: item.crossMinSize,
@ -114,6 +110,7 @@ const FlexboxActor = ActorClassWithSpec(flexboxSpec, {
mainDeltaSize: item.mainDeltaSize,
mainMaxSize: item.mainMaxSize,
mainMinSize: item.mainMinSize,
lineGrowthState: line.growthState,
}));
}
}
@ -157,32 +154,50 @@ const FlexItemActor = ActorClassWithSpec(flexItemSpec, {
return this.actorID;
}
const { flexDirection } = CssLogic.getComputedStyle(this.containerEl);
const dimension = flexDirection.startsWith("row") ? "width" : "height";
// Find the authored sizing properties for this item.
const properties = {
"flex-basis": "",
"flex-grow": "",
"flex-shrink": "",
[`min-${dimension}`]: "",
[`max-${dimension}`]: "",
[dimension]: ""
};
if (this.element.nodeType === this.element.ELEMENT_NODE) {
for (const name in properties) {
let value = "";
// Look first on the element style.
if (this.element.style[name] && this.element.style[name] !== "auto") {
value = this.element.style[name];
} else {
// And then on the rules that apply to the element.
// getCSSStyleRules returns rules from least to most specific, so override
// values as we find them.
const cssRules = getCSSStyleRules(this.element);
for (const rule of cssRules) {
const rulePropertyValue = rule.style.getPropertyValue(name);
if (rulePropertyValue && rulePropertyValue !== "auto") {
value = rulePropertyValue;
}
}
}
properties[name] = value;
}
}
const form = {
actor: this.actorID,
// The flex item sizing data.
flexItemSizing: this.flexItemSizing,
// The authored style properties of the flex item.
properties,
};
if (this.element.nodeType === ELEMENT_NODE) {
const { flexDirection } = CssLogic.getComputedStyle(this.containerEl);
const styles = CssLogic.getComputedStyle(this.element);
const clientRect = this.element.getBoundingClientRect();
const dimension = flexDirection.startsWith("row") ? "width" : "height";
// The computed style properties of the flex item.
form.properties = {
"flex-basis": styles.flexBasis,
"flex-grow": styles.flexGrow,
"flex-shrink": styles.flexShrink,
// min-width/height computed style.
[`min-${dimension}`]: styles[`min-${dimension}`],
// max-width/height computed style.
[`max-${dimension}`]: styles[`max-${dimension}`],
// Computed width/height of the flex item element.
[dimension]: parseFloat(clientRect[dimension.toLowerCase()].toPrecision(6)),
};
}
// If the WalkerActor already knows the flex item element, then also return its
// ActorID so we avoid the client from doing another round trip to get it in many
// cases.

View File

@ -65,7 +65,7 @@ class EventEmitter {
return;
}
if (length === 3) {
if (length >= 3) {
// Trying to remove from the `target` the `listener` specified for the
// event's `type` given.
const listenersForType = events.get(type);

View File

@ -70,7 +70,7 @@ const FlexItemFront = FrontClassWithSpec(flexItemSpec, {
},
/**
* Get the computed style properties for the flex item.
* Get the style properties for the flex item.
*/
get properties() {
return this._form.properties;

View File

@ -323,6 +323,13 @@ const TESTS = {
await Promise.all([pFoo, pDone]);
},
testCallingOffWithMoreThan3Args() {
const target = { name: "target"};
on(target, "data", fail);
off(target, "data", fail, undefined);
emit(target, "data", "Listener should be removed");
}
};
/**

View File

@ -132,8 +132,7 @@ nsStyledElement::SetInlineStyleDeclaration(DeclarationBlock& aDeclaration,
nsIDocument* document = GetComposedDoc();
mozAutoDocUpdate updateBatch(document, true);
return SetAttrAndNotify(kNameSpaceID_None, nsGkAtoms::style, nullptr,
aData.mOldValue.isSome() ?
aData.mOldValue.ptr() : nullptr,
aData.mOldValue.ptrOr(nullptr),
attrValue, nullptr, aData.mModType,
hasListeners, true, kDontCallAfterSetAttr,
document, updateBatch);

View File

@ -546,10 +546,6 @@ DOMInterfaces = {
'headerFile': 'MediaStreamList.h',
},
'MediaStreamTrack': {
'concrete': False
},
'MediaRecorder': {
'headerFile': 'MediaRecorder.h',
},

View File

@ -13,6 +13,7 @@
#include "mozilla/dom/HTMLMediaElement.h"
#include "AudioChannelService.h"
#include "AudioDeviceInfo.h"
#include "AudioStreamTrack.h"
#include "AutoplayPolicy.h"
#include "ChannelMediaDecoder.h"
@ -31,6 +32,7 @@
#include "MP4Decoder.h"
#include "MediaContainerType.h"
#include "MediaError.h"
#include "MediaManager.h"
#include "MediaMetadataManager.h"
#include "MediaResource.h"
#include "MediaSourceDecoder.h"
@ -3885,6 +3887,7 @@ HTMLMediaElement::HTMLMediaElement(
, mPaused(true, "HTMLMediaElement::mPaused")
, mErrorSink(new ErrorSink(this))
, mAudioChannelWrapper(new AudioChannelAgentCallback(this))
, mSink(MakePair(nsString(), RefPtr<AudioDeviceInfo>()))
{
MOZ_ASSERT(mMainThreadEventTarget);
MOZ_ASSERT(mAbstractMainThread);
@ -5085,6 +5088,19 @@ HTMLMediaElement::FinishDecoderSetup(MediaDecoder* aDecoder)
// can affect how we feed data to MediaStreams
NotifyDecoderPrincipalChanged();
// Set sink device if we have one. Otherwise the default is used.
if (mSink.second()) {
mDecoder->SetSink(mSink.second())
#ifdef DEBUG
->Then(mAbstractMainThread, __func__,
[](const GenericPromise::ResolveOrRejectValue& aValue) {
MOZ_ASSERT(aValue.IsResolve() && !aValue.ResolveValue());
});
#else
;
#endif
}
for (OutputMediaStream& ms : mOutputStreams) {
if (ms.mCapturingMediaStream) {
MOZ_ASSERT(!ms.mCapturingDecoder);
@ -5316,6 +5332,9 @@ HTMLMediaElement::UpdateSrcMediaStreamPlaying(uint32_t aFlags)
stream->AddAudioOutput(this);
SetVolumeInternal();
if (mSink.second()) {
NS_WARNING("setSinkId() when playing a MediaStream is not supported yet and will be ignored");
}
VideoFrameContainer* container = GetVideoFrameContainer();
if (mSelectedVideoStreamTrack && container) {
@ -8265,6 +8284,86 @@ HTMLMediaElement::ReportCanPlayTelemetry()
NS_DISPATCH_NORMAL);
}
already_AddRefed<Promise>
HTMLMediaElement::SetSinkId(const nsAString& aSinkId, ErrorResult& aRv)
{
nsCOMPtr<nsPIDOMWindowInner> win = OwnerDoc()->GetInnerWindow();
if (!win) {
aRv.Throw(NS_ERROR_UNEXPECTED);
return nullptr;
}
RefPtr<Promise> promise = Promise::Create(win->AsGlobal(), aRv);
if (aRv.Failed()) {
return nullptr;
}
if (mSink.first().Equals(aSinkId)) {
promise->MaybeResolveWithUndefined();
return promise.forget();
}
nsString sinkId(aSinkId);
MediaManager::Get()->GetSinkDevice(win, sinkId)
->Then(mAbstractMainThread, __func__,
[self = RefPtr<HTMLMediaElement>(this)](RefPtr<AudioDeviceInfo>&& aInfo) {
// Sink found switch output device.
MOZ_ASSERT(aInfo);
if (self->mDecoder) {
RefPtr<SinkInfoPromise> p = self->mDecoder->SetSink(aInfo)
->Then(self->mAbstractMainThread, __func__,
[aInfo] (const GenericPromise::ResolveOrRejectValue& aValue) {
if (aValue.IsResolve()) {
return SinkInfoPromise::CreateAndResolve(aInfo, __func__);
}
return SinkInfoPromise::CreateAndReject(aValue.RejectValue(), __func__);
});
return p;
}
if (self->GetSrcMediaStream()) {
// Set Sink Id through MSG is not supported yet.
return SinkInfoPromise::CreateAndReject(NS_ERROR_ABORT, __func__);
}
// No media attached to the element save it for later.
return SinkInfoPromise::CreateAndResolve(aInfo, __func__);
},
[](nsresult res){
// Promise is rejected, sink not found.
return SinkInfoPromise::CreateAndReject(res, __func__);
})
->Then(mAbstractMainThread, __func__,
[promise, self = RefPtr<HTMLMediaElement>(this),
sinkId = std::move(sinkId)] (const SinkInfoPromise::ResolveOrRejectValue& aValue) {
if (aValue.IsResolve()) {
self->mSink = MakePair(sinkId, aValue.ResolveValue());
promise->MaybeResolveWithUndefined();
} else {
switch (aValue.RejectValue()) {
case NS_ERROR_ABORT:
promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
break;
case NS_ERROR_NOT_AVAILABLE:
{
ErrorResult notFoundError;
notFoundError.ThrowDOMException(
NS_ERROR_DOM_NOT_FOUND_ERR,
NS_LITERAL_CSTRING("The object can not be found here."));
promise->MaybeReject(notFoundError);
break;
}
case NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR:
promise->MaybeReject(NS_ERROR_DOM_NOT_ALLOWED_ERR);
break;
default:
MOZ_ASSERT_UNREACHABLE("Invalid error.");
}
}
});
aRv = NS_OK;
return promise.forget();
}
} // namespace dom
} // namespace mozilla

View File

@ -66,6 +66,7 @@ class VideoStreamTrack;
} // namespace dom
} // namespace mozilla
class AudioDeviceInfo;
class nsIChannel;
class nsIHttpChannel;
class nsILoadGroup;
@ -827,6 +828,17 @@ public:
return mMainThreadEventTarget;
}
// Set the sink id (of the output device) that the audio will play. If aSinkId
// is empty the default device will be set.
already_AddRefed<Promise> SetSinkId(const nsAString& aSinkId, ErrorResult& aRv);
// Get the sink id of the device that audio is being played. Initial value is
// empty and the default device is being used.
void GetSinkId(nsString& aSinkId)
{
MOZ_ASSERT(NS_IsMainThread());
aSinkId = mSink.first();
}
protected:
virtual ~HTMLMediaElement();
@ -1893,6 +1905,14 @@ private:
// Attach UA Shadow Root if it is not attached.
void AttachAndSetUAShadowRoot();
// Contains the unique id of the sink device and the device info.
// The initial value is ("", nullptr) and the default output device is used.
// It can contain an invalid id and info if the device has been
// unplugged. It can be set to ("", nullptr). It follows the spec attribute:
// https://w3c.github.io/mediacapture-output/#htmlmediaelement-extensions
// Read/Write from the main thread only.
Pair<nsString, RefPtr<AudioDeviceInfo>> mSink;
};
// Check if the context is chrome or has the debugger or tabs permission

View File

@ -337,7 +337,8 @@ int AudioStream::InvokeCubeb(Function aFunction, Args&&... aArgs)
nsresult
AudioStream::Init(uint32_t aNumChannels,
AudioConfig::ChannelLayout::ChannelMap aChannelMap,
uint32_t aRate)
uint32_t aRate,
AudioDeviceInfo* aSinkInfo)
{
auto startTime = TimeStamp::Now();
@ -347,6 +348,8 @@ AudioStream::Init(uint32_t aNumChannels,
mDumpFile = OpenDumpFile(aNumChannels, aRate);
mSinkInfo = aSinkInfo;
cubeb_stream_params params;
params.rate = aRate;
params.channels = mOutChannels;
@ -380,8 +383,12 @@ AudioStream::OpenCubeb(cubeb* aContext, cubeb_stream_params& aParams,
/* Convert from milliseconds to frames. */
uint32_t latency_frames =
CubebUtils::GetCubebPlaybackLatencyInMilliseconds() * aParams.rate / 1000;
cubeb_devid deviceID = nullptr;
if (mSinkInfo && mSinkInfo->DeviceID()) {
deviceID = mSinkInfo->DeviceID();
}
if (cubeb_stream_init(aContext, &stream, "AudioStream",
nullptr, nullptr, nullptr, &aParams,
nullptr, nullptr, deviceID, &aParams,
latency_frames,
DataCallback_S, StateCallback_S, this) == CUBEB_OK) {
mCubebStream.reset(stream);
@ -411,7 +418,7 @@ AudioStream::SetVolume(double aVolume)
}
}
void
nsresult
AudioStream::Start()
{
MonitorAutoLock mon(mMonitor);
@ -422,6 +429,10 @@ AudioStream::Start()
mState = ERRORED;
}
LOG("started, state %s", mState == STARTED ? "STARTED" : mState == DRAINED ? "DRAINED" : "ERRORED");
if (mState == STARTED || mState == DRAINED) {
return NS_OK;
}
return NS_ERROR_FAILURE;
}
void

View File

@ -200,7 +200,8 @@ public:
// (22050Hz, 44100Hz, etc).
nsresult Init(uint32_t aNumChannels,
AudioConfig::ChannelLayout::ChannelMap aChannelMap,
uint32_t aRate);
uint32_t aRate,
AudioDeviceInfo* aSinkInfo);
// Closes the stream. All future use of the stream is an error.
void Shutdown();
@ -212,7 +213,7 @@ public:
void SetVolume(double aVolume);
// Start the stream.
void Start();
nsresult Start();
// Pause audio playback.
void Pause();
@ -317,6 +318,11 @@ private:
DataSource& mDataSource;
bool mPrefillQuirk;
// The device info of the current sink. If null
// the default device is used. It is set
// during the Init() in decoder thread.
RefPtr<AudioDeviceInfo> mSinkInfo;
};
} // namespace mozilla

View File

@ -7,17 +7,10 @@
#include "nsContentUtils.h"
#include "mozilla/dom/AudioStreamTrackBinding.h"
namespace mozilla {
namespace dom {
JSObject*
AudioStreamTrack::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
{
return AudioStreamTrack_Binding::Wrap(aCx, this, aGivenProto);
}
void
AudioStreamTrack::GetLabel(nsAString& aLabel, CallerType aCallerType)
{

View File

@ -20,10 +20,7 @@ public:
const MediaTrackConstraints& aConstraints = MediaTrackConstraints())
: MediaStreamTrack(aStream, aTrackID, aInputTrackID, aSource, aConstraints) {}
JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
AudioStreamTrack* AsAudioStreamTrack() override { return this; }
const AudioStreamTrack* AsAudioStreamTrack() const override { return this; }
// WebIDL

View File

@ -657,27 +657,45 @@ DOMMediaStream::GetId(nsAString& aID) const
}
void
DOMMediaStream::GetAudioTracks(nsTArray<RefPtr<AudioStreamTrack> >& aTracks) const
DOMMediaStream::GetAudioTracks(nsTArray<RefPtr<AudioStreamTrack>>& aTracks) const
{
for (const RefPtr<TrackPort>& info : mTracks) {
AudioStreamTrack* t = info->GetTrack()->AsAudioStreamTrack();
if (t) {
if (AudioStreamTrack* t = info->GetTrack()->AsAudioStreamTrack()) {
aTracks.AppendElement(t);
}
}
}
void
DOMMediaStream::GetVideoTracks(nsTArray<RefPtr<VideoStreamTrack> >& aTracks) const
DOMMediaStream::GetAudioTracks(nsTArray<RefPtr<MediaStreamTrack>>& aTracks) const
{
for (const RefPtr<TrackPort>& info : mTracks) {
VideoStreamTrack* t = info->GetTrack()->AsVideoStreamTrack();
if (t) {
if (info->GetTrack()->AsAudioStreamTrack()) {
aTracks.AppendElement(info->GetTrack());
}
}
}
void
DOMMediaStream::GetVideoTracks(nsTArray<RefPtr<VideoStreamTrack>>& aTracks) const
{
for (const RefPtr<TrackPort>& info : mTracks) {
if (VideoStreamTrack* t = info->GetTrack()->AsVideoStreamTrack()) {
aTracks.AppendElement(t);
}
}
}
void
DOMMediaStream::GetVideoTracks(nsTArray<RefPtr<MediaStreamTrack>>& aTracks) const
{
for (const RefPtr<TrackPort>& info : mTracks) {
if (info->GetTrack()->AsVideoStreamTrack()) {
aTracks.AppendElement(info->GetTrack());
}
}
}
void
DOMMediaStream::GetTracks(nsTArray<RefPtr<MediaStreamTrack> >& aTracks) const
{

View File

@ -367,7 +367,9 @@ public:
void GetId(nsAString& aID) const;
void GetAudioTracks(nsTArray<RefPtr<AudioStreamTrack> >& aTracks) const;
void GetAudioTracks(nsTArray<RefPtr<MediaStreamTrack> >& aTracks) const;
void GetVideoTracks(nsTArray<RefPtr<VideoStreamTrack> >& aTracks) const;
void GetVideoTracks(nsTArray<RefPtr<MediaStreamTrack> >& aTracks) const;
void GetTracks(nsTArray<RefPtr<MediaStreamTrack> >& aTracks) const;
MediaStreamTrack* GetTrackById(const nsAString& aId) const;
void AddTrack(MediaStreamTrack& aTrack);

View File

@ -169,6 +169,14 @@ MediaDecoder::SetVolume(double aVolume)
mVolume = aVolume;
}
RefPtr<GenericPromise>
MediaDecoder::SetSink(AudioDeviceInfo* aSink)
{
MOZ_ASSERT(NS_IsMainThread());
AbstractThread::AutoEnter context(AbstractMainThread());
return GetStateMachine()->InvokeSetSink(aSink);
}
void
MediaDecoder::AddOutputStream(ProcessedMediaStream* aStream,
TrackID aNextAvailableTrackID,

View File

@ -157,6 +157,9 @@ public:
void SetPreservesPitch(bool aPreservesPitch);
void SetLooping(bool aLooping);
// Set the given device as the output device.
RefPtr<GenericPromise> SetSink(AudioDeviceInfo* aSink);
bool GetMinimizePreroll() const { return mMinimizePreroll; }
// All MediaStream-related data is protected by mReentrantMonitor.

View File

@ -3303,40 +3303,44 @@ MediaDecoderStateMachine::WaitForData(MediaData::Type aType)
}
}
void
nsresult
MediaDecoderStateMachine::StartMediaSink()
{
MOZ_ASSERT(OnTaskQueue());
if (!mMediaSink->IsStarted()) {
mAudioCompleted = false;
mMediaSink->Start(GetMediaTime(), Info());
auto videoPromise = mMediaSink->OnEnded(TrackInfo::kVideoTrack);
auto audioPromise = mMediaSink->OnEnded(TrackInfo::kAudioTrack);
if (audioPromise) {
audioPromise->Then(
OwnerThread(), __func__, this,
&MediaDecoderStateMachine::OnMediaSinkAudioComplete,
&MediaDecoderStateMachine::OnMediaSinkAudioError)
->Track(mMediaSinkAudioPromise);
}
if (videoPromise) {
videoPromise->Then(
OwnerThread(), __func__, this,
&MediaDecoderStateMachine::OnMediaSinkVideoComplete,
&MediaDecoderStateMachine::OnMediaSinkVideoError)
->Track(mMediaSinkVideoPromise);
}
// Remember the initial offset when playback starts. This will be used
// to calculate the rate at which bytes are consumed as playback moves on.
RefPtr<MediaData> sample = mAudioQueue.PeekFront();
mPlaybackOffset = sample ? sample->mOffset : 0;
sample = mVideoQueue.PeekFront();
if (sample && sample->mOffset > mPlaybackOffset) {
mPlaybackOffset = sample->mOffset;
}
if (mMediaSink->IsStarted()) {
return NS_OK;
}
mAudioCompleted = false;
nsresult rv = mMediaSink->Start(GetMediaTime(), Info());
auto videoPromise = mMediaSink->OnEnded(TrackInfo::kVideoTrack);
auto audioPromise = mMediaSink->OnEnded(TrackInfo::kAudioTrack);
if (audioPromise) {
audioPromise->Then(
OwnerThread(), __func__, this,
&MediaDecoderStateMachine::OnMediaSinkAudioComplete,
&MediaDecoderStateMachine::OnMediaSinkAudioError)
->Track(mMediaSinkAudioPromise);
}
if (videoPromise) {
videoPromise->Then(
OwnerThread(), __func__, this,
&MediaDecoderStateMachine::OnMediaSinkVideoComplete,
&MediaDecoderStateMachine::OnMediaSinkVideoError)
->Track(mMediaSinkVideoPromise);
}
// Remember the initial offset when playback starts. This will be used
// to calculate the rate at which bytes are consumed as playback moves on.
RefPtr<MediaData> sample = mAudioQueue.PeekFront();
mPlaybackOffset = sample ? sample->mOffset : 0;
sample = mVideoQueue.PeekFront();
if (sample && sample->mOffset > mPlaybackOffset) {
mPlaybackOffset = sample->mOffset;
}
return rv;
}
bool
@ -3664,6 +3668,60 @@ MediaDecoderStateMachine::LoopingChanged()
}
}
RefPtr<GenericPromise>
MediaDecoderStateMachine::InvokeSetSink(RefPtr<AudioDeviceInfo> aSink)
{
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(aSink);
++mSetSinkRequestsCount;
return InvokeAsync(
OwnerThread(), this, __func__,
&MediaDecoderStateMachine::SetSink, aSink);
}
RefPtr<GenericPromise>
MediaDecoderStateMachine::SetSink(RefPtr<AudioDeviceInfo> aSink)
{
MOZ_ASSERT(OnTaskQueue());
if (mAudioCaptured) {
// Not supported yet.
return GenericPromise::CreateAndReject(NS_ERROR_ABORT, __func__);
}
// Backup current playback parameters.
bool wasPlaying = mMediaSink->IsPlaying();
if (--mSetSinkRequestsCount > 0) {
MOZ_ASSERT(mSetSinkRequestsCount > 0);
return GenericPromise::CreateAndResolve(wasPlaying, __func__);
}
MediaSink::PlaybackParams params = mMediaSink->GetPlaybackParams();
params.mSink = std::move(aSink);
if (!mMediaSink->IsStarted()) {
mMediaSink->SetPlaybackParams(params);
return GenericPromise::CreateAndResolve(false, __func__);
}
// Stop and shutdown the existing sink.
StopMediaSink();
mMediaSink->Shutdown();
// Create a new sink according to whether audio is captured.
mMediaSink = CreateMediaSink(false);
// Restore playback parameters.
mMediaSink->SetPlaybackParams(params);
// Start the new sink
if (wasPlaying) {
nsresult rv = StartMediaSink();
if (NS_FAILED(rv)) {
return GenericPromise::CreateAndReject(NS_ERROR_ABORT, __func__);
}
}
return GenericPromise::CreateAndResolve(wasPlaying, __func__);
}
TimeUnit
MediaDecoderStateMachine::AudioEndTime() const
{

View File

@ -296,6 +296,8 @@ public:
// Sets the video decode mode. Used by the suspend-video-decoder feature.
void SetVideoDecodeMode(VideoDecodeMode aMode);
RefPtr<GenericPromise> InvokeSetSink(RefPtr<AudioDeviceInfo> aSink);
private:
class StateObject;
class DecodeMetadataState;
@ -369,6 +371,16 @@ private:
void SetVideoDecodeModeInternal(VideoDecodeMode aMode);
// Set new sink device and restart MediaSink if playback is started.
// Returned promise will be resolved with true if the playback is
// started and false if playback is stopped after setting the new sink.
// Returned promise will be rejected with value NS_ERROR_ABORT
// if the action fails or it is not supported.
// If there are multiple pending requests only the last one will be
// executed, for all previous requests the promise will be resolved
// with true or false similar to above.
RefPtr<GenericPromise> SetSink(RefPtr<AudioDeviceInfo> aSink);
protected:
virtual ~MediaDecoderStateMachine();
@ -447,7 +459,8 @@ protected:
// Create and start the media sink.
// The decoder monitor must be held with exactly one lock count.
// Called on the state machine thread.
void StartMediaSink();
// If start fails an NS_ERROR_FAILURE is returned.
nsresult StartMediaSink();
// Notification method invoked when mPlayState changes.
void PlayStateChanged();
@ -739,6 +752,9 @@ private:
// Used to distinguish whether the audio is producing sound.
Canonical<bool> mIsAudioDataAudible;
// Used to count the number of pending requests to set a new sink.
Atomic<int> mSetSinkRequestsCount;
public:
AbstractCanonical<media::TimeIntervals>* CanonicalBuffered() const;

View File

@ -916,11 +916,12 @@ private:
*/
NS_IMPL_ISUPPORTS(MediaDevice, nsIMediaDevice)
MediaDevice::MediaDevice(MediaEngineSource* aSource,
MediaDevice::MediaDevice(const RefPtr<MediaEngineSource>& aSource,
const nsString& aName,
const nsString& aID,
const nsString& aRawID)
: mSource(aSource)
, mSinkInfo(nullptr)
, mKind((mSource && MediaEngineSource::IsVideo(mSource->GetMediaSource())) ?
dom::MediaDeviceKind::Videoinput : dom::MediaDeviceKind::Audioinput)
, mScary(mSource->GetScary())
@ -932,15 +933,16 @@ MediaDevice::MediaDevice(MediaEngineSource* aSource,
MOZ_ASSERT(mSource);
}
MediaDevice::MediaDevice(const nsString& aName,
const dom::MediaDeviceKind aKind,
MediaDevice::MediaDevice(const RefPtr<AudioDeviceInfo>& aAudioDeviceInfo,
const nsString& aID,
const nsString& aRawID)
: mSource(nullptr)
, mKind(aKind)
, mSinkInfo(aAudioDeviceInfo)
, mKind(mSinkInfo->Type() == AudioDeviceInfo::TYPE_INPUT ? dom::MediaDeviceKind::Audioinput
: dom::MediaDeviceKind::Audiooutput)
, mScary(false)
, mType(NS_ConvertUTF8toUTF16(dom::MediaDeviceKindValues::strings[uint32_t(mKind)].value))
, mName(aName)
, mName(mSinkInfo->Name())
, mID(aID)
, mRawID(aRawID)
{
@ -949,12 +951,14 @@ MediaDevice::MediaDevice(const nsString& aName,
// when we do not instantiate a MediaEngineSource
// during EnumerateDevices.
MOZ_ASSERT(mKind == dom::MediaDeviceKind::Audiooutput);
MOZ_ASSERT(mSinkInfo);
}
MediaDevice::MediaDevice(const MediaDevice* aOther,
MediaDevice::MediaDevice(const RefPtr<MediaDevice>& aOther,
const nsString& aID,
const nsString& aRawID)
: mSource(aOther->mSource)
, mSinkInfo(aOther->mSinkInfo)
, mKind(aOther->mKind)
, mScary(aOther->mScary)
, mType(aOther->mType)
@ -3418,6 +3422,61 @@ MediaManager::EnumerateDevices(nsPIDOMWindowInner* aWindow,
return NS_OK;
}
RefPtr<SinkInfoPromise>
MediaManager::GetSinkDevice(nsPIDOMWindowInner* aWindow,
const nsString& aDeviceId)
{
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(aWindow);
// We have to add the window id here because enumerate methods
// check for that and abort silently if it does not exist.
uint64_t windowId = aWindow->WindowID();
nsIPrincipal* principal = aWindow->GetExtantDoc()->NodePrincipal();
RefPtr<GetUserMediaWindowListener> windowListener = GetWindowListener(windowId);
if (windowListener) {
PrincipalHandle existingPrincipalHandle =
windowListener->GetPrincipalHandle();
MOZ_ASSERT(PrincipalHandleMatches(existingPrincipalHandle, principal));
} else {
windowListener = new GetUserMediaWindowListener(mMediaThread, windowId,
MakePrincipalHandle(principal));
AddWindowID(windowId, windowListener);
}
// Create an inactive SourceListener to act as a placeholder, so the
// window listener doesn't clean itself up until we're done.
RefPtr<SourceListener> sourceListener = new SourceListener();
windowListener->Register(sourceListener);
bool isSecure = aWindow->IsSecureContext();
return EnumerateDevicesImpl(aWindow->WindowID(),
MediaSourceEnum::Other,
MediaSourceEnum::Other,
MediaSinkEnum::Speaker,
DeviceEnumerationType::Normal,
DeviceEnumerationType::Normal)
->Then(GetCurrentThreadSerialEventTarget(), __func__,
[aDeviceId, isSecure](RefPtr<MediaDeviceSetRefCnt>&& aDevices) {
for (RefPtr<MediaDevice>& device : **aDevices) {
if (aDeviceId.IsEmpty() && device->mSinkInfo->Preferred()) {
return SinkInfoPromise::CreateAndResolve(device->mSinkInfo, __func__);
}
if (device->mID.Equals(aDeviceId)) {
// TODO: Check if the application is authorized to play audio
// through this device (Bug 1493982).
if (isSecure || device->mSinkInfo->Preferred()) {
return SinkInfoPromise::CreateAndResolve(device->mSinkInfo, __func__);
}
return SinkInfoPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR, __func__);
}
}
return SinkInfoPromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE, __func__);
}, [](RefPtr<MediaStreamError>&& reason) {
return SinkInfoPromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE, __func__);
});
}
/*
* GetUserMediaDevices - called by the UI-part of getUserMedia from chrome JS.
*/

View File

@ -71,17 +71,16 @@ public:
NS_DECL_THREADSAFE_ISUPPORTS
NS_DECL_NSIMEDIADEVICE
explicit MediaDevice(MediaEngineSource* aSource,
explicit MediaDevice(const RefPtr<MediaEngineSource>& aSource,
const nsString& aName,
const nsString& aID,
const nsString& aRawID = NS_LITERAL_STRING(""));
const nsString& aRawID);
explicit MediaDevice(const nsString& aName,
const dom::MediaDeviceKind aKind,
explicit MediaDevice(const RefPtr<AudioDeviceInfo>& aAudioDeviceInfo,
const nsString& aID,
const nsString& aRawID = NS_LITERAL_STRING(""));
explicit MediaDevice(const MediaDevice* aOther,
explicit MediaDevice(const RefPtr<MediaDevice>& aOther,
const nsString& aID,
const nsString& aRawID);
@ -129,6 +128,7 @@ private:
public:
const RefPtr<MediaEngineSource> mSource;
const RefPtr<AudioDeviceInfo> mSinkInfo;
const dom::MediaDeviceKind mKind;
const bool mScary;
const nsString mType;
@ -138,6 +138,7 @@ public:
};
typedef nsRefPtrHashtable<nsUint64HashKey, GetUserMediaWindowListener> WindowTable;
typedef MozPromise<RefPtr<AudioDeviceInfo>, nsresult, true> SinkInfoPromise;
class MediaManager final : public nsIMediaManagerService,
public nsIObserver
@ -227,6 +228,26 @@ public:
dom::CallerType aCallerType);
nsresult EnumerateDevices(nsPIDOMWindowInner* aWindow, dom::Promise& aPromise);
// Get the sink that corresponds to the given device id.
// It is resposible to check if an application is
// authorized to play audio through the requested device.
// The returned promise will be resolved with the device
// information if the device id matches one and operation is
// allowed. The default device is always allowed. Non default
// devices are allowed only in secure context. It is pending to
// implement an user authorization model. The promise will be
// rejected in the following cases:
// NS_ERROR_NOT_AVAILABLE: Device id does not exist.
// NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR:
// The requested device exists but it is not allowed to be used.
// Currently, this happens only on non-default default devices
// and non https connections. TODO, authorization model to allow
// an application to play audio through the device (Bug 1493982).
// NS_ERROR_ABORT: General error.
RefPtr<SinkInfoPromise> GetSinkDevice(nsPIDOMWindowInner* aWindow,
const nsString& aDeviceId);
void OnNavigation(uint64_t aWindowID);
bool IsActivelyCapturingOrHasAPermission(uint64_t aWindowId);

View File

@ -211,6 +211,12 @@ MediaStreamTrack::GetParentObject() const
return mOwningStream->GetParentObject();
}
JSObject*
MediaStreamTrack::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
{
return MediaStreamTrack_Binding::Wrap(aCx, this, aGivenProto);
}
void
MediaStreamTrack::GetId(nsAString& aID) const
{

View File

@ -386,7 +386,7 @@ public:
DOMEventTargetHelper)
nsPIDOMWindowInner* GetParentObject() const;
virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override = 0;
JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
virtual AudioStreamTrack* AsAudioStreamTrack() { return nullptr; }
virtual VideoStreamTrack* AsVideoStreamTrack() { return nullptr; }

View File

@ -9,17 +9,9 @@
#include "MediaStreamGraph.h"
#include "nsContentUtils.h"
#include "mozilla/dom/VideoStreamTrackBinding.h"
namespace mozilla {
namespace dom {
JSObject*
VideoStreamTrack::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
{
return VideoStreamTrack_Binding::Wrap(aCx, this, aGivenProto);
}
void
VideoStreamTrack::AddVideoOutput(MediaStreamVideoSink* aSink)
{

View File

@ -23,10 +23,7 @@ public:
const MediaTrackConstraints& aConstraints = MediaTrackConstraints())
: MediaStreamTrack(aStream, aTrackID, aInputTrackID, aSource, aConstraints) {}
JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
VideoStreamTrack* AsVideoStreamTrack() override { return this; }
const VideoStreamTrack* AsVideoStreamTrack() const override { return this; }
void AddVideoOutput(MediaStreamVideoSink* aSink);

View File

@ -79,7 +79,7 @@ CaptureTask::AttachTrack()
{
MOZ_ASSERT(NS_IsMainThread());
dom::VideoStreamTrack* track = mImageCapture->GetVideoStreamTrack();
dom::MediaStreamTrack* track = mImageCapture->GetVideoStreamTrack();
track->AddPrincipalChangeObserver(this);
track->AddListener(mEventListener.get());
track->AddDirectListener(this);
@ -90,7 +90,7 @@ CaptureTask::DetachTrack()
{
MOZ_ASSERT(NS_IsMainThread());
dom::VideoStreamTrack* track = mImageCapture->GetVideoStreamTrack();
dom::MediaStreamTrack* track = mImageCapture->GetVideoStreamTrack();
track->RemovePrincipalChangeObserver(this);
track->RemoveListener(mEventListener.get());
track->RemoveDirectListener(this);

View File

@ -28,7 +28,7 @@ LogModule* GetICLog()
namespace dom {
NS_IMPL_CYCLE_COLLECTION_INHERITED(ImageCapture, DOMEventTargetHelper,
mVideoStreamTrack)
mTrack)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ImageCapture)
NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
@ -36,14 +36,13 @@ NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
NS_IMPL_ADDREF_INHERITED(ImageCapture, DOMEventTargetHelper)
NS_IMPL_RELEASE_INHERITED(ImageCapture, DOMEventTargetHelper)
ImageCapture::ImageCapture(VideoStreamTrack* aVideoStreamTrack,
ImageCapture::ImageCapture(VideoStreamTrack* aTrack,
nsPIDOMWindowInner* aOwnerWindow)
: DOMEventTargetHelper(aOwnerWindow)
, mTrack(aTrack)
{
MOZ_ASSERT(aOwnerWindow);
MOZ_ASSERT(aVideoStreamTrack);
mVideoStreamTrack = aVideoStreamTrack;
MOZ_ASSERT(aTrack);
}
ImageCapture::~ImageCapture()
@ -53,7 +52,7 @@ ImageCapture::~ImageCapture()
already_AddRefed<ImageCapture>
ImageCapture::Constructor(const GlobalObject& aGlobal,
VideoStreamTrack& aTrack,
MediaStreamTrack& aTrack,
ErrorResult& aRv)
{
nsCOMPtr<nsPIDOMWindowInner> win = do_QueryInterface(aGlobal.GetAsSupports());
@ -62,15 +61,20 @@ ImageCapture::Constructor(const GlobalObject& aGlobal,
return nullptr;
}
RefPtr<ImageCapture> object = new ImageCapture(&aTrack, win);
if (!aTrack.AsVideoStreamTrack()) {
aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
return nullptr;
}
RefPtr<ImageCapture> object = new ImageCapture(aTrack.AsVideoStreamTrack(), win);
return object.forget();
}
VideoStreamTrack*
MediaStreamTrack*
ImageCapture::GetVideoStreamTrack() const
{
return mVideoStreamTrack;
return mTrack;
}
nsresult
@ -118,25 +122,25 @@ ImageCapture::TakePhotoByMediaEngine()
mVideoTrack->RemovePrincipalChangeObserver(this);
}
RefPtr<VideoStreamTrack> mVideoTrack;
RefPtr<ImageCapture> mImageCapture;
const RefPtr<VideoStreamTrack> mVideoTrack;
const RefPtr<ImageCapture> mImageCapture;
bool mPrincipalChanged;
};
RefPtr<MediaEnginePhotoCallback> callback =
new TakePhotoCallback(mVideoStreamTrack, this);
return mVideoStreamTrack->GetSource().TakePhoto(callback);
new TakePhotoCallback(mTrack, this);
return mTrack->GetSource().TakePhoto(callback);
}
void
ImageCapture::TakePhoto(ErrorResult& aResult)
{
// According to spec, VideoStreamTrack.readyState must be "live"; however
// According to spec, MediaStreamTrack.readyState must be "live"; however
// gecko doesn't implement it yet (bug 910249). Instead of readyState, we
// check VideoStreamTrack.enable before bug 910249 is fixed.
// check MediaStreamTrack.enable before bug 910249 is fixed.
// The error code should be INVALID_TRACK, but spec doesn't define it in
// ImageCaptureError. So it returns PHOTO_ERROR here before spec updates.
if (!mVideoStreamTrack->Enabled()) {
if (!mTrack->Enabled()) {
PostErrorEvent(ImageCaptureError::PHOTO_ERROR, NS_ERROR_FAILURE);
return;
}
@ -211,7 +215,7 @@ ImageCapture::CheckPrincipal()
{
MOZ_ASSERT(NS_IsMainThread());
nsCOMPtr<nsIPrincipal> principal = mVideoStreamTrack->GetPrincipal();
nsCOMPtr<nsIPrincipal> principal = mTrack->GetPrincipal();
if (!GetOwner()) {
return false;

View File

@ -21,20 +21,21 @@ LogModule* GetICLog();
namespace dom {
class Blob;
class MediaStreamTrack;
class VideoStreamTrack;
/**
* Implementation of https://dvcs.w3.org/hg/dap/raw-file/default/media-stream-
* capture/ImageCapture.html.
* The ImageCapture accepts a VideoStreamTrack as input source. The image will
* be sent back as a JPG format via Blob event.
* Implementation of https://dvcs.w3.org/hg/dap/raw-file/default/media-stream-
* capture/ImageCapture.html.
* The ImageCapture accepts a video MediaStreamTrack as input source. The image
* will be sent back as a JPG format via Blob event.
*
* All the functions in ImageCapture are run in main thread.
* All the functions in ImageCapture are run in main thread.
*
* There are two ways to capture image, MediaEngineSource and MediaStreamGraph.
* When the implementation of MediaEngineSource supports TakePhoto(),
* it uses the platform camera to grab image. Otherwise, it falls back
* to the MediaStreamGraph way.
* There are two ways to capture image, MediaEngineSource and MediaStreamGraph.
* When the implementation of MediaEngineSource supports TakePhoto(),
* it uses the platform camera to grab image. Otherwise, it falls back
* to the MediaStreamGraph way.
*/
class ImageCapture final : public DOMEventTargetHelper
@ -49,8 +50,8 @@ public:
// WebIDL members.
void TakePhoto(ErrorResult& aResult);
// The MediaStream passed into the constructor.
VideoStreamTrack* GetVideoStreamTrack() const;
// The MediaStreamTrack passed into the constructor.
MediaStreamTrack* GetVideoStreamTrack() const;
// nsWrapperCache member
JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override
@ -62,10 +63,10 @@ public:
nsPIDOMWindowInner* GetParentObject() { return GetOwner(); }
static already_AddRefed<ImageCapture> Constructor(const GlobalObject& aGlobal,
VideoStreamTrack& aTrack,
MediaStreamTrack& aTrack,
ErrorResult& aRv);
ImageCapture(VideoStreamTrack* aVideoStreamTrack,
ImageCapture(VideoStreamTrack* aTrack,
nsPIDOMWindowInner* aOwnerWindow);
// Post a Blob event to script.
@ -85,7 +86,7 @@ protected:
// should return NS_ERROR_NOT_IMPLEMENTED.
nsresult TakePhotoByMediaEngine();
RefPtr<VideoStreamTrack> mVideoStreamTrack;
RefPtr<VideoStreamTrack> mTrack;
};
} // namespace dom

View File

@ -71,8 +71,8 @@ AudioSink::~AudioSink()
{
}
RefPtr<GenericPromise>
AudioSink::Init(const PlaybackParams& aParams)
nsresult
AudioSink::Init(const PlaybackParams& aParams, RefPtr<GenericPromise>& aEndPromise)
{
MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
@ -86,12 +86,12 @@ AudioSink::Init(const PlaybackParams& aParams)
// To ensure at least one audio packet will be popped from AudioQueue and
// ready to be played.
NotifyAudioNeeded();
RefPtr<GenericPromise> p = mEndPromise.Ensure(__func__);
aEndPromise = mEndPromise.Ensure(__func__);
nsresult rv = InitializeAudioStream(aParams);
if (NS_FAILED(rv)) {
mEndPromise.Reject(rv, __func__);
}
return p;
return rv;
}
TimeUnit
@ -197,7 +197,8 @@ AudioSink::InitializeAudioStream(const PlaybackParams& aParams)
// mOutputChannels into SMPTE format, so there is no need to worry if
// StaticPrefs::accessibility_monoaudio_enable() or
// StaticPrefs::MediaForcestereoEnabled() is applied.
nsresult rv = mAudioStream->Init(mOutputChannels, channelMap, mOutputRate);
nsresult rv = mAudioStream->Init(mOutputChannels, channelMap,
mOutputRate, aParams.mSink);
if (NS_FAILED(rv)) {
mAudioStream->Shutdown();
mAudioStream = nullptr;
@ -209,9 +210,7 @@ AudioSink::InitializeAudioStream(const PlaybackParams& aParams)
mAudioStream->SetVolume(aParams.mVolume);
mAudioStream->SetPlaybackRate(aParams.mPlaybackRate);
mAudioStream->SetPreservesPitch(aParams.mPreservesPitch);
mAudioStream->Start();
return NS_OK;
return mAudioStream->Start();
}
TimeUnit

View File

@ -38,7 +38,7 @@ public:
// Return a promise which will be resolved when AudioSink
// finishes playing, or rejected if any error.
RefPtr<GenericPromise> Init(const PlaybackParams& aParams);
nsresult Init(const PlaybackParams& aParams, RefPtr<GenericPromise>& aEndPromise);
/*
* All public functions are not thread-safe.

View File

@ -178,7 +178,7 @@ AudioSinkWrapper::SetPlaying(bool aPlaying)
}
}
void
nsresult
AudioSinkWrapper::Start(const TimeUnit& aStartTime, const MediaInfo& aInfo)
{
AssertOwnerThread();
@ -191,9 +191,10 @@ AudioSinkWrapper::Start(const TimeUnit& aStartTime, const MediaInfo& aInfo)
// no audio is equivalent to audio ended before video starts.
mAudioEnded = !aInfo.HasAudio();
nsresult rv = NS_OK;
if (aInfo.HasAudio()) {
mAudioSink.reset(mCreator->Create());
mEndPromise = mAudioSink->Init(mParams);
rv = mAudioSink->Init(mParams, mEndPromise);
mEndPromise->Then(
mOwnerThread.get(), __func__, this,
@ -201,6 +202,7 @@ AudioSinkWrapper::Start(const TimeUnit& aStartTime, const MediaInfo& aInfo)
&AudioSinkWrapper::OnAudioEnded
)->Track(mAudioSinkPromise);
}
return rv;
}
void

View File

@ -67,7 +67,7 @@ public:
void SetPreservesPitch(bool aPreservesPitch) override;
void SetPlaying(bool aPlaying) override;
void Start(const TimeUnit& aStartTime, const MediaInfo& aInfo) override;
nsresult Start(const TimeUnit& aStartTime, const MediaInfo& aInfo) override;
void Stop() override;
bool IsStarted() const override;
bool IsPlaying() const override;

View File

@ -304,7 +304,7 @@ DecodedStream::OnEnded(TrackType aType)
return nullptr;
}
void
nsresult
DecodedStream::Start(const TimeUnit& aStartTime, const MediaInfo& aInfo)
{
AssertOwnerThread();
@ -371,6 +371,7 @@ DecodedStream::Start(const TimeUnit& aStartTime, const MediaInfo& aInfo)
mData->SetPlaying(mPlaying);
SendData();
}
return NS_OK;
}
void

View File

@ -61,7 +61,7 @@ public:
void SetPreservesPitch(bool aPreservesPitch) override;
void SetPlaying(bool aPlaying) override;
void Start(const media::TimeUnit& aStartTime, const MediaInfo& aInfo) override;
nsresult Start(const media::TimeUnit& aStartTime, const MediaInfo& aInfo) override;
void Stop() override;
bool IsStarted() const override;
bool IsPlaying() const override;

View File

@ -7,10 +7,11 @@
#ifndef MediaSink_h_
#define MediaSink_h_
#include "AudioDeviceInfo.h"
#include "MediaInfo.h"
#include "mozilla/RefPtr.h"
#include "mozilla/MozPromise.h"
#include "nsISupportsImpl.h"
#include "MediaInfo.h"
namespace mozilla {
@ -44,6 +45,7 @@ public:
double mVolume;
double mPlaybackRate;
bool mPreservesPitch;
RefPtr<AudioDeviceInfo> mSink;
};
// Return the playback parameters of this sink.
@ -100,7 +102,7 @@ public:
// Begin a playback session with the provided start time and media info.
// Must be called when playback is stopped.
virtual void Start(const TimeUnit& aStartTime, const MediaInfo& aInfo) = 0;
virtual nsresult Start(const TimeUnit& aStartTime, const MediaInfo& aInfo) = 0;
// Finish a playback session.
// Must be called after playback starts.

View File

@ -205,13 +205,13 @@ VideoSink::SetPlaying(bool aPlaying)
EnsureHighResTimersOnOnlyIfPlaying();
}
void
nsresult
VideoSink::Start(const TimeUnit& aStartTime, const MediaInfo& aInfo)
{
AssertOwnerThread();
VSINK_LOG("[%s]", __func__);
mAudioSink->Start(aStartTime, aInfo);
nsresult rv = mAudioSink->Start(aStartTime, aInfo);
mHasVideo = aInfo.HasVideo();
@ -247,6 +247,7 @@ VideoSink::Start(const TimeUnit& aStartTime, const MediaInfo& aInfo)
// when video duration is 0.
UpdateRenderedVideoFrames();
}
return rv;
}
void

View File

@ -58,7 +58,7 @@ public:
void Redraw(const VideoInfo& aInfo) override;
void Start(const TimeUnit& aStartTime, const MediaInfo& aInfo) override;
nsresult Start(const TimeUnit& aStartTime, const MediaInfo& aInfo) override;
void Stop() override;

View File

@ -97,6 +97,7 @@ EXPORTS += [
'AudioCompactor.h',
'AudioConfig.h',
'AudioConverter.h',
'AudioDeviceInfo.h',
'AudioMixer.h',
'AudioPacketizer.h',
'AudioSampleFormat.h',

View File

@ -358,3 +358,6 @@ skip-if = (android_version == '18')
[test_peerConnection_nonDefaultRate.html]
skip-if = android_version == '18' # android(Bug 1189784, timeouts on 4.3 emulator)
[test_forceSampleRate.html]
[test_setSinkId.html]
skip-if = os != 'linux' # the only platform with real devices

View File

@ -0,0 +1,55 @@
<!DOCTYPE HTML>
<html>
<head>
<script type="application/javascript" src="mediaStreamPlayback.js"></script>
</head>
<body>
<pre id="test">
<audio id="audio"></audio>
<script>
createHTML({
title: "SetSinkId in HTMLMediaElement",
bug: "934425",
});
/**
* Run a test to verify set sink id in audio element.
*/
runTest(async () => {
await pushPrefs(["media.setsinkid.enabled", true]);
if (!SpecialPowers.getCharPref("media.audio_loopback_dev", "")) {
ok(false, "No loopback device set by framework. Try --use-test-media-devices");
return;
}
const allDevices = await navigator.mediaDevices.enumerateDevices();
const audioDevices = allDevices.filter(({kind}) => kind == 'audiooutput');
info(`Found ${audioDevices.length} output devices`);
ok(audioDevices.length > 0, "More than one output device found");
is(audio.sinkId, "", "Initial value is empty string");
const p = audio.setSinkId(audioDevices[0].deviceId);
is(audio.sinkId, "", "Value is unchanged upon function return");
is(await p, undefined, "promise resolves with undefined");
is(audio.sinkId, audioDevices[0].deviceId, `Sink device is set, id: ${audio.sinkId}`);
await audio.setSinkId(audioDevices[0].deviceId);
ok(true, `Sink device is set for 2nd time for the same id: ${audio.sinkId}`);
try {
await audio.setSinkId("dummy sink id");
ok(false, "Never enter here, this must fail");
} catch (error) {
ok(true, `Set sink id expected to fail: ${error}`);
is(error.name, "NotFoundError", "Verify correct error");
}
});
</script>
</pre>
</body>
</html>

View File

@ -580,7 +580,8 @@ MediaEngineDefault::EnumerateDevices(uint64_t aWindowId,
aDevices->AppendElement(MakeRefPtr<MediaDevice>(
newSource,
newSource->GetName(),
NS_ConvertUTF8toUTF16(newSource->GetUUID())));
NS_ConvertUTF8toUTF16(newSource->GetUUID()),
NS_LITERAL_STRING("")));
return;
}
case dom::MediaSourceEnum::Microphone: {
@ -591,7 +592,8 @@ MediaEngineDefault::EnumerateDevices(uint64_t aWindowId,
aDevices->AppendElement(MakeRefPtr<MediaDevice>(
source,
source->GetName(),
NS_ConvertUTF8toUTF16(source->GetUUID())));
NS_ConvertUTF8toUTF16(source->GetUUID()),
NS_LITERAL_STRING("")));
}
}
@ -602,7 +604,8 @@ MediaEngineDefault::EnumerateDevices(uint64_t aWindowId,
aDevices->AppendElement(MakeRefPtr<MediaDevice>(
newSource,
newSource->GetName(),
NS_ConvertUTF8toUTF16(newSource->GetUUID())));
NS_ConvertUTF8toUTF16(newSource->GetUUID()),
NS_LITERAL_STRING("")));
}
return;
}

View File

@ -166,7 +166,8 @@ MediaEngineWebRTC::EnumerateVideoDevices(uint64_t aWindowId,
aDevices->AppendElement(MakeRefPtr<MediaDevice>(
vSource,
vSource->GetName(),
NS_ConvertUTF8toUTF16(vSource->GetUUID())));
NS_ConvertUTF8toUTF16(vSource->GetUUID()),
NS_LITERAL_STRING("")));
}
if (mHasTabVideoSource || dom::MediaSourceEnum::Browser == aMediaSource) {
@ -174,7 +175,8 @@ MediaEngineWebRTC::EnumerateVideoDevices(uint64_t aWindowId,
aDevices->AppendElement(MakeRefPtr<MediaDevice>(
tabVideoSource,
tabVideoSource->GetName(),
NS_ConvertUTF8toUTF16(tabVideoSource->GetUUID())));
NS_ConvertUTF8toUTF16(tabVideoSource->GetUUID()),
NS_LITERAL_STRING("")));
}
}
@ -218,7 +220,8 @@ MediaEngineWebRTC::EnumerateMicrophoneDevices(uint64_t aWindowId,
RefPtr<MediaDevice> device = MakeRefPtr<MediaDevice>(
source,
source->GetName(),
NS_ConvertUTF8toUTF16(source->GetUUID()));
NS_ConvertUTF8toUTF16(source->GetUUID()),
NS_LITERAL_STRING(""));
if (devices[i]->Preferred()) {
#ifdef DEBUG
if (!foundPreferredDevice) {
@ -251,10 +254,7 @@ MediaEngineWebRTC::EnumerateSpeakerDevices(uint64_t aWindowId,
// would be the same for both which ends up to create the same
// deviceIDs (in JS).
uuid.Append(NS_LITERAL_STRING("_Speaker"));
aDevices->AppendElement(MakeRefPtr<MediaDevice>(
device->Name(),
dom::MediaDeviceKind::Audiooutput,
uuid));
aDevices->AppendElement(MakeRefPtr<MediaDevice>(device, uuid));
}
}
}
@ -278,7 +278,8 @@ MediaEngineWebRTC::EnumerateDevices(uint64_t aWindowId,
aDevices->AppendElement(MakeRefPtr<MediaDevice>(
audioCaptureSource,
audioCaptureSource->GetName(),
NS_ConvertUTF8toUTF16(audioCaptureSource->GetUUID())));
NS_ConvertUTF8toUTF16(audioCaptureSource->GetUUID()),
NS_LITERAL_STRING("")));
} else if (aMediaSource == dom::MediaSourceEnum::Microphone) {
MOZ_ASSERT(aMediaSource == dom::MediaSourceEnum::Microphone);
EnumerateMicrophoneDevices(aWindowId, aDevices);

View File

@ -620,7 +620,8 @@ MediaConstraintsHelper::FindBadConstraint(
AutoTArray<RefPtr<MediaDevice>, 1> devices;
devices.AppendElement(MakeRefPtr<MediaDevice>(aMediaEngineSource,
aMediaEngineSource->GetName(),
aDeviceId));
aDeviceId,
NS_LITERAL_STRING("")));
return FindBadConstraint(aConstraints, devices);
}

View File

@ -152,8 +152,6 @@ var interfaceNamesInGlobalScope =
{name: "AudioProcessingEvent", insecureContext: true},
// IMPORTANT: Do not change this list without review from a DOM peer!
{name: "AudioScheduledSourceNode", insecureContext: true},
// IMPORTANT: Do not change this list without review from a DOM peer!
{name: "AudioStreamTrack", insecureContext: true},
// IMPORTANT: Do not change this list without review from a DOM peer!
{name: "AudioWorkletNode", insecureContext: false, disabled: true},
// IMPORTANT: Do not change this list without review from a DOM peer!
@ -1154,8 +1152,6 @@ var interfaceNamesInGlobalScope =
{name: "ValidityState", insecureContext: true},
// IMPORTANT: Do not change this list without review from a DOM peer!
{name: "VideoPlaybackQuality", insecureContext: true},
// IMPORTANT: Do not change this list without review from a DOM peer!
{name: "VideoStreamTrack", insecureContext: true},
// IMPORTANT: Do not change this list without review from a DOM peer!
{name: "VisualViewport", insecureContext: true},
// IMPORTANT: Do not change this list without review from a DOM peer!

View File

@ -1,16 +0,0 @@
/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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/.
*
* The origin of this IDL file is
* http://dev.w3.org/2011/webrtc/editor/getusermedia.html
*
* Copyright © 2012 W3C® (MIT, ERCIM, Keio), All Rights Reserved. W3C
* liability, trademark and document use rules apply.
*/
// [Constructor(optional MediaTrackConstraints audioConstraints)]
interface AudioStreamTrack : MediaStreamTrack {
// static sequence<DOMString> getSourceIds ();
};

View File

@ -220,6 +220,14 @@ partial interface HTMLMediaElement {
boolean hasSuspendTaint();
};
/* Audio Output Devices API */
partial interface HTMLMediaElement {
[Pref="media.setsinkid.enabled"]
readonly attribute DOMString sinkId;
[Throws, Pref="media.setsinkid.enabled"]
Promise<void> setSinkId(DOMString sinkId);
};
/*
* API that exposes whether a call to HTMLMediaElement.play() would be
* blocked by autoplay policies; whether the promise returned by play()

View File

@ -10,10 +10,10 @@
* W3C liability, trademark and document use rules apply.
*/
[Pref="dom.imagecapture.enabled", Constructor(VideoStreamTrack track)]
[Pref="dom.imagecapture.enabled", Constructor(MediaStreamTrack track)]
interface ImageCapture : EventTarget {
// readonly attribute PhotoSettingsOptions photoSettingsOptions;
readonly attribute VideoStreamTrack videoStreamTrack;
readonly attribute MediaStreamTrack videoStreamTrack;
attribute EventHandler onphoto;
attribute EventHandler onerror;
// attribute EventHandler onphotosettingschange;

View File

@ -29,8 +29,8 @@ dictionary MediaStreamConstraints {
Constructor (sequence<MediaStreamTrack> tracks)]
interface MediaStream : EventTarget {
readonly attribute DOMString id;
sequence<AudioStreamTrack> getAudioTracks ();
sequence<VideoStreamTrack> getVideoTracks ();
sequence<MediaStreamTrack> getAudioTracks ();
sequence<MediaStreamTrack> getVideoTracks ();
sequence<MediaStreamTrack> getTracks ();
MediaStreamTrack? getTrackById (DOMString trackId);
void addTrack (MediaStreamTrack track);

View File

@ -1,19 +0,0 @@
/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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/.
*
* The origin of this IDL file is
* http://dev.w3.org/2011/webrtc/editor/getusermedia.html
*
* Copyright © 2012 W3C® (MIT, ERCIM, Keio), All Rights Reserved. W3C
* liability, trademark and document use rules apply.
*/
// [Constructor(optional MediaTrackConstraints videoConstraints)]
interface VideoStreamTrack : MediaStreamTrack {
// static sequence<DOMString> getSourceIds ();
// void takePhoto ();
// attribute EventHandler onphoto;
// attribute EventHandler onphotoerror;
};

View File

@ -387,7 +387,6 @@ WEBIDL_FILES = [
'AudioParamMap.webidl',
'AudioProcessingEvent.webidl',
'AudioScheduledSourceNode.webidl',
'AudioStreamTrack.webidl',
'AudioTrack.webidl',
'AudioTrackList.webidl',
'AudioWorklet.webidl',
@ -918,7 +917,6 @@ WEBIDL_FILES = [
'URLSearchParams.webidl',
'ValidityState.webidl',
'VideoPlaybackQuality.webidl',
'VideoStreamTrack.webidl',
'VideoTrack.webidl',
'VideoTrackList.webidl',
'VisualViewport.webidl',

View File

@ -841,6 +841,14 @@ gfxPlatform::Init()
/* this currently will only succeed on Windows */
gfxInfo = services::GetGfxInfo();
if (XRE_IsParentProcess()) {
// Some gfxVars must be initialized prior gPlatform for coherent results.
gfxVars::SetDXInterop2Blocked(IsDXInterop2Blocked());
gfxVars::SetDXNV12Blocked(IsDXNV12Blocked());
gfxVars::SetDXP010Blocked(IsDXP010Blocked());
gfxVars::SetDXP016Blocked(IsDXP016Blocked());
}
#if defined(XP_WIN)
gPlatform = new gfxWindowsPlatform;
#elif defined(XP_MACOSX)
@ -961,10 +969,6 @@ gfxPlatform::Init()
InitOpenGLConfig();
if (XRE_IsParentProcess()) {
gfxVars::SetDXInterop2Blocked(IsDXInterop2Blocked());
gfxVars::SetDXNV12Blocked(IsDXNV12Blocked());
gfxVars::SetDXP010Blocked(IsDXP010Blocked());
gfxVars::SetDXP016Blocked(IsDXP016Blocked());
Preferences::Unlock(FONT_VARIATIONS_PREF);
if (!gPlatform->HasVariationFontSupport()) {
// Ensure variation fonts are disabled and the pref is locked.

View File

@ -78,12 +78,9 @@ nsDOMCSSAttributeDeclaration::SetCSSDeclaration(DeclarationBlock* aDecl,
// needed.
MOZ_ASSERT_IF(!mIsSMILOverride, aClosureData);
// If the closure hasn't been called because the declaration wasn't changed,
// we need to explicitly call it now to get InlineStyleDeclarationWillChange
// notification before SetInlineStyleDeclaration.
if (aClosureData && aClosureData->mClosure) {
aClosureData->mClosure(aClosureData);
}
// The closure needs to have been called by now, otherwise we shouldn't be
// getting here when the attribute hasn't changed.
MOZ_ASSERT_IF(aClosureData, !aClosureData->mClosure);
aDecl->SetDirty();
return mIsSMILOverride

View File

@ -127,12 +127,6 @@ nsDOMCSSDeclaration::SetCssText(const nsAString& aCssText,
ParsingEnvironment servoEnv =
GetParsingEnvironment(aSubjectPrincipal);
if (!servoEnv.mUrlExtraData) {
if (created) {
// In case we can't set a new declaration, but one was
// created for the old one, we need to set the old declaration to
// get right style attribute handling.
SetCSSDeclaration(olddecl, &closureData);
}
aRv.Throw(NS_ERROR_NOT_AVAILABLE);
return;
}
@ -287,22 +281,12 @@ nsDOMCSSDeclaration::ModifyDeclaration(nsIPrincipal* aSubjectPrincipal,
ParsingEnvironment servoEnv =
GetParsingEnvironment(aSubjectPrincipal);
if (!servoEnv.mUrlExtraData) {
if (created) {
// In case we can't set a new declaration, but one was
// created for the old one, we need to set the old declaration to
// get right style attribute handling.
SetCSSDeclaration(olddecl, aClosureData);
}
return NS_ERROR_NOT_AVAILABLE;
}
changed = aFunc(decl, servoEnv);
if (!changed) {
if (created) {
// See comment above about setting old declaration.
SetCSSDeclaration(olddecl, aClosureData);
}
// Parsing failed -- but we don't throw an exception for that.
return NS_OK;
}

View File

@ -156,7 +156,7 @@ public final class NotificationClient implements NotificationListener {
if (!AppConstants.Versions.preO) {
builder.setChannelId(NotificationHelper.getInstance(mContext)
.getNotificationChannel(NotificationHelper.Channel.DEFAULT).getId());
.getNotificationChannel(NotificationHelper.Channel.SITE_NOTIFICATIONS).getId());
}
// Fetch icon.

View File

@ -116,6 +116,10 @@ public final class NotificationHelper implements BundleEventListener {
* Leanplum notification channel - use only when <code>AppConstants.MOZ_ANDROID_MMA</code> is true.
*/
LP_DEFAULT,
/**
* HTML5 web site notifications
*/
SITE_NOTIFICATIONS,
}
// Holds the mapping between the Channel enum used by the rest of our codebase and the
@ -143,6 +147,9 @@ public final class NotificationHelper implements BundleEventListener {
final String SYNCED_TABS_CHANNEL_TAG = "synced-tabs-notification-channel";
put(Channel.SYNCED_TABS, SYNCED_TABS_CHANNEL_TAG);
final String SITE_NOTIFICATIONS_CHANNEL_TAG = "site-notifications";
put(Channel.SITE_NOTIFICATIONS, SITE_NOTIFICATIONS_CHANNEL_TAG);
}};
// These are channels we no longer require and want to retire from Android's settings UI.
@ -261,6 +268,13 @@ public final class NotificationHelper implements BundleEventListener {
}
break;
case SITE_NOTIFICATIONS: {
channel = new NotificationChannel(mDefinedNotificationChannels.get(definedChannel),
mContext.getString(R.string.site_notifications_channel),
NotificationManager.IMPORTANCE_DEFAULT);
}
break;
case DEFAULT:
default: {
channel = new NotificationChannel(mDefinedNotificationChannels.get(definedChannel),

View File

@ -901,3 +901,6 @@ Picture-in-picture mini window -->
<!ENTITY leanplum_default_notifications_channel "&brandShortName; Push notifications">
<!ENTITY updater_notification_channel "App updates">
<!ENTITY synced_tabs_notification_channel "Synced tabs">
<!-- LOCALIZATION NOTE (site_notifications_channel): This is for system notifications displayed by
web sites through the HTML Notifications API. -->
<!ENTITY site_notifications_channel "Site notifications">

View File

@ -653,4 +653,5 @@
<string name="leanplum_default_notifications_channel">&leanplum_default_notifications_channel;</string>
<string name="updater_notification_channel">&updater_notification_channel;</string>
<string name="synced_tabs_notification_channel">&synced_tabs_notification_channel;</string>
<string name="site_notifications_channel">&site_notifications_channel;</string>
</resources>

View File

@ -13,7 +13,7 @@ class LSANLeaks(object):
in allocation stacks
"""
def __init__(self, logger, scope=None, allowed=None):
def __init__(self, logger, scope=None, allowed=None, maxNumRecordedFrames=None):
self.logger = logger
self.inReport = False
self.fatalError = False
@ -21,7 +21,7 @@ class LSANLeaks(object):
self.foundFrames = set()
self.recordMoreFrames = None
self.currStack = None
self.maxNumRecordedFrames = 4
self.maxNumRecordedFrames = maxNumRecordedFrames if maxNumRecordedFrames else 4
self.summaryData = None
self.scope = scope
self.allowedMatch = None
@ -136,6 +136,7 @@ class LSANLeaks(object):
self.logger.info("LeakSanitizer | To show the "
"addresses of leaked objects add report_objects=1 to LSAN_OPTIONS\n"
"This can be done in testing/mozbase/mozrunner/mozrunner/utils.py")
self.logger.info("Allowed depth was %d" % self.maxNumRecordedFrames)
for frames, allowed in self.foundFrames:
self.logger.lsan_leak(frames, scope=self.scope, allowed_match=allowed)

View File

@ -34,6 +34,8 @@ user_pref("extensions.legacy.enabled", true);
user_pref("extensions.update.enabled", false);
// Disable useragent updates.
user_pref("general.useragent.updates.enabled", false);
// Ensure WR doesn't get enabled in tests unless we do it explicitly with the MOZ_WEBRENDER envvar.
user_pref("gfx.webrender.all.qualified", false);
user_pref("hangmonitor.timeout", 0); // no hang monitor
user_pref("media.gmp-manager.updateEnabled", false);
// Make enablePrivilege continue to work for test code. :-(

View File

@ -74,6 +74,12 @@
"c:\\windows\\prefetch\\{prefetch}.pf": {
"ignore": true
},
"c:\\windows\\system32\\apphelp.dll": {
"mincount": 0,
"maxcount": 2,
"minbytes": 0,
"maxbytes": 32768
},
"c:\\windows\\system32\\windows.storage.dll": {
"mincount": 2,
"maxcount": 2,

View File

@ -327250,6 +327250,12 @@
{}
]
],
"audio-output/setSinkId.html": [
[
"/audio-output/setSinkId.html",
{}
]
],
"audio-output/setSinkId.https.html": [
[
"/audio-output/setSinkId.https.html",
@ -442345,6 +442351,10 @@
"a083cdf09232110039d3bb825e207c678b336114",
"manual"
],
"audio-output/setSinkId.html": [
"bd5d8e43b0fd9d0c9f1e078ed97a1bbd18b7b0be",
"testharness"
],
"audio-output/setSinkId.https.html": [
"2ce0b482b7eea61c0c56c49ec14dc1630b7b9a9c",
"testharness"

View File

@ -0,0 +1,2 @@
[setSinkId.html]
prefs: [media.setsinkid.enabled:true]

View File

@ -1,10 +1,2 @@
[setSinkId.https.html]
[setSinkId on default audio output should always work]
expected: FAIL
[setSinkId fails with NotFoundError on made up deviceid]
expected: FAIL
[List media devices]
expected: FAIL
prefs: [media.setsinkid.enabled:true]

View File

@ -1,4 +0,0 @@
[cssstyledeclaration-mutationrecord-002.html]
[CSSStyleDeclaration.setPropertyValue doesn't queue a mutation record when setting invalid values]
expected: FAIL

View File

@ -5,16 +5,10 @@
[MediaStreamTrack interface: attribute onoverconstrained]
expected: FAIL
[MediaStreamTrack must be primary interface of [object AudioStreamTrack\]]
[MediaStreamTrack interface: [object MediaStreamTrack\] must inherit property "getCapabilities()" with the proper type]
expected: FAIL
[Stringification of [object AudioStreamTrack\]]
expected: FAIL
[MediaStreamTrack interface: [object AudioStreamTrack\] must inherit property "getCapabilities()" with the proper type]
expected: FAIL
[MediaStreamTrack interface: [object AudioStreamTrack\] must inherit property "onoverconstrained" with the proper type]
[MediaStreamTrack interface: [object MediaStreamTrack\] must inherit property "onoverconstrained" with the proper type]
expected: FAIL
[OverconstrainedErrorEvent interface: existence and properties of interface object]

View File

@ -1,2 +1,2 @@
prefs: [dom.serviceWorkers.enabled:true]
lsan-allowed: [Alloc, Create, CreateInner, MakeUnique, Malloc, NewChannelFromURIWithProxyFlagsInternal, NewEmptyScopeData, NewPage, OrInsert, PLDHashTable::Add, Realloc, SharedMutex, __rdl_alloc, __rdl_realloc, js_new, js_pod_calloc, js_pod_malloc, js_pod_realloc, mozilla::BasePrincipal::CreateCodebasePrincipal, mozilla::ThrottledEventQueue::Create, mozilla::WeakPtr, mozilla::dom::ChromeUtils::GenerateQI, mozilla::dom::Performance::CreateForMainThread, mozilla::dom::PerformanceStorageWorker::Create, mozilla::dom::WorkerPrivate::WorkerPrivate, mozilla::net::HttpBaseChannel::HttpBaseChannel, mozilla::net::HttpChannelChild::HttpChannelChild, mozilla::net::nsHttpAuthIdentity::Set, mozilla::net::nsHttpHandler::NewProxiedChannel2, nsNodeSupportsWeakRefTearoff::GetWeakReference, nsPrefetchService::Preload, nsSegmentedBuffer::AppendNewSegment]
lsan-allowed: [Alloc, Create, CreateInner, MakeUnique, Malloc, NewChannelFromURIWithProxyFlagsInternal, NewEmptyScopeData, NewPage, OrInsert, PLDHashTable::Add, Realloc, SharedMutex, __rdl_alloc, __rdl_realloc, js_new, js_pod_calloc, js_pod_malloc, js_pod_realloc, mozilla::BasePrincipal::CreateCodebasePrincipal, mozilla::ThrottledEventQueue::Create, mozilla::WeakPtr, mozilla::dom::ChromeUtils::GenerateQI, mozilla::dom::Performance::CreateForMainThread, mozilla::dom::PerformanceStorageWorker::Create, mozilla::dom::WorkerPrivate::WorkerPrivate, mozilla::net::HttpBaseChannel::HttpBaseChannel, mozilla::net::HttpChannelChild::HttpChannelChild, mozilla::net::nsHttpAuthIdentity::Set, mozilla::net::nsHttpHandler::NewProxiedChannel2, nsNodeSupportsWeakRefTearoff::GetWeakReference, nsPrefetchService::Preload, nsSegmentedBuffer::AppendNewSegment, nsDocShell::Create]

View File

@ -1 +1,2 @@
lsan-allowed: [Alloc, Create, Malloc, NewPage, PLDHashTable::Add, PLDHashTable::ChangeTable, Realloc, RecvOnAcknowledge, RecvOnStop, mozilla::BasePrincipal::CreateCodebasePrincipal, mozilla::SchedulerGroup::CreateEventTargetFor, mozilla::ThrottledEventQueue::Create, mozilla::WeakPtr, mozilla::dom::WebSocket::WebSocket, mozilla::dom::WorkerCSPEventListener::Create, mozilla::dom::nsIContentChild::GetConstructedEventTarget, mozilla::net::WebSocketChannelChild::RecvOnServerClose, nsAtomTable::Atomize, mozilla::net::nsStandardURL::TemplatedMutator]
lsan-allowed: [Alloc, Create, Malloc, NewPage, PLDHashTable::Add, PLDHashTable::ChangeTable, Realloc, RecvOnAcknowledge, RecvOnStop, mozilla::BasePrincipal::CreateCodebasePrincipal, mozilla::SchedulerGroup::CreateEventTargetFor, mozilla::ThrottledEventQueue::Create, mozilla::WeakPtr, mozilla::dom::WebSocket::WebSocket, mozilla::dom::WorkerCSPEventListener::Create, mozilla::dom::nsIContentChild::GetConstructedEventTarget, mozilla::net::WebSocketChannelChild::RecvOnServerClose, nsAtomTable::Atomize, mozilla::net::nsStandardURL::TemplatedMutator, nsDocShell::Create]
lsan-max-stack-depth: 7

View File

@ -0,0 +1,44 @@
<!doctype html>
<meta charset=utf-8>
<title>Test setSinkId behavior </title>
<div id='log'></div>
<script src=/resources/testharness.js></script>
<script src=/resources/testharnessreport.js></script>
<script>
"use strict";
const audio = new Audio();
promise_test(t => audio.setSinkId(""), "setSinkId on default audio output should always work");
promise_test(t => promise_rejects(t, "NotFoundError", audio.setSinkId("nonexistent_device_id")),
"setSinkId fails with NotFoundError on made up deviceid");
promise_test(async t => {
const list = await navigator.mediaDevices.enumerateDevices();
const outputDevicesList = list.filter(({kind}) => kind == "audiooutput");
assert_not_equals(outputDevicesList.length, 0,
"media device list includes at least one audio output device");
let acceptedDevices = 0;
for (const {deviceId} of outputDevicesList) {
const {deviceId} = outputDevicesList[0];
const p1 = audio.setSinkId(deviceId);
assert_equals(audio.sinkId, "", "before it resolves, setSinkId is unchanged");
try {
let r = await p1;
assert_equals(acceptedDevices, 0, "only the default sink device can be set");
acceptedDevices++;
assert_equals(r, undefined, "setSinkId resolves with undefined");
assert_equals(audio.sinkId, deviceId, "when it resolves, setSinkId updates sinkId to the requested deviceId");
r = await audio.setSinkId(deviceId);
assert_equals(r, undefined, "resetting sinkid on same current value should always work");
r = await audio.setSinkId("");
assert_equals(r, undefined, "resetting sinkid on default audio output should always work");
} catch (e) {
assert_equals(e.name, "NotAllowedError", "Non-default devices are failing with NotAllowed error");
}
}
}, "List device, setSinkId should on the default, the rest of the devices will get a NotAlowedError");
</script>

View File

@ -1,45 +1,46 @@
<!doctype html>
<html>
<head>
<title>Test setSinkId behavior </title>
<link rel="author" title="Dominique Hazael-Massieux" href="mailto:dom@w3.org"/>
<link rel="help" href="https://www.w3.org/TR/audio-output/#dom-htmlmediaelement-setsinkid">
</head>
<body>
<h1 class="instructions">Description</h1>
<p class="instructions">This test checks that <code>setSinkId</code> follows the algorithm (but does not consider actual rendering of the audio which needs to be manual).</p>
<div id='log'></div>
<script src=/resources/testharness.js></script>
<script src=/resources/testharnessreport.js></script>
<script>
"use strict";
const is_output = d => d.kind === "audiooutput";
const audio = new Audio();
promise_test(t => audio.setSinkId(""), "setSinkId on default audio output should always work");
promise_test(t => promise_rejects(t, "NotFoundError", audio.setSinkId("inexistent_device_id")), "setSinkId fails with NotFoundError on made up deviceid");
promise_test(t => promise_rejects(t, "NotFoundError", audio.setSinkId("nonexistent_device_id")),
"setSinkId fails with NotFoundError on made up deviceid");
promise_test(t =>
navigator.mediaDevices.enumerateDevices().then(list => {
assert_not_equals(list.find(is_output), undefined, "media device list includes at least one audio output device");
// since we haven't gained any specific permission,
// for all listed audio output devices, calling setSinkId with device id can
// either create a security exception or work and thus reflect the deviceId
let acceptedDevice = 0;
list.filter(is_output).forEach((d,i) => promise_test(td => audio.setSinkId(d.deviceId).then(r => {
assert_equals(r, undefined, "setSinkId resolves with undefined");
assert_equals(audio.sinkId, d.deviceId, "when it resolves, setSinkId updates sinkId to the requested deviceId");
assert_equals(acceptedDevice, 0, "only one output device can be set without permission");
acceptedDevice++;
promise_test(t => audio.setSinkId(d.deviceId), "resetting sinkid on same current value should always work");
promise_test(t => audio.setSinkId(""), "resetting sinkid on default audio output should always work");
}, e => {
assert_equals(e.name, "SecurityError", "On known devices, the only possible failure of setSinkId is a securityerror"); // assuming AbortError can't happen in the test environment by default
}), "Correctly reacts to setting known deviceid as sinkid " + i));
}), "List media devices");
promise_test(async t => {
const list = await navigator.mediaDevices.enumerateDevices();
const outputDevicesList = list.filter(({kind}) => kind == "audiooutput");
assert_not_equals(outputDevicesList.length, 0,
"media device list includes at least one audio output device");
let acceptedDevices = 0;
for (const {deviceId} of outputDevicesList) {
const {deviceId} = outputDevicesList[0];
const p1 = audio.setSinkId(deviceId);
assert_equals(audio.sinkId, "", "before it resolves, setSinkId is unchanged");
try {
let r = await p1;
assert_equals(acceptedDevices, 0, "only the default sink device can be set");
acceptedDevices++;
assert_equals(r, undefined, "setSinkId resolves with undefined");
assert_equals(audio.sinkId, deviceId, "when it resolves, setSinkId updates sinkId to the requested deviceId");
r = await audio.setSinkId(deviceId);
assert_equals(r, undefined, "resetting sinkid on same current value should always work");
r = await audio.setSinkId("");
assert_equals(r, undefined, "resetting sinkid on default audio output should always work");
} catch (e) {
assert_equals(e.name, "NotAllowedError", "Non-default devices are failing with NotAllowed error");
}
}
}, "List device, setSinkId should on the default, the rest of the devices will get a NotAlowedError");
</script>
</body>
</html>

View File

@ -1,7 +1,7 @@
<!doctype html>
<meta charset="utf-8">
<title>CSSOM: CSSStyleDeclaration.setPropertyValue doesn't queue a mutation record for invalid values</title>
<link rel="help" href="https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-setpropertyvalue">
<link rel="help" href="https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-setproperty">
<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
<script src=/resources/testharness.js></script>
<script src=/resources/testharnessreport.js></script>

View File

@ -0,0 +1,16 @@
<!doctype html>
<html style="color: inherit">
<meta charset="utf-8">
<title>CSSOM: CSSStyleDeclaration.setPropertyValue doesn't queue a mutation record for invalid values</title>
<link rel="help" href="https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-setproperty">
<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
<script src=/resources/testharness.js></script>
<script src=/resources/testharnessreport.js></script>
<script>
let test = async_test("CSSStyleDeclaration.setPropertyValue doesn't queue a mutation record when setting invalid values");
let m = new MutationObserver(test.unreached_func("shouldn't queue a mutation record"));
m.observe(document.documentElement, { attributes: true });
document.documentElement.style.width = "-100px";
requestAnimationFrame(() => test.done());
</script>

View File

@ -0,0 +1,18 @@
<!doctype html>
<title>CSSOM test: declaration block after setting via CSSOM</title>
<link rel="help" href="https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-setproperty">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
<script>
test(function() {
let element = document.createElement("div");
element.style.setProperty("doesntexist", "0");
assert_false(element.hasAttribute("style"));
}, "Setting an invalid property via the declaration setter doesn't create a declaration");
test(function() {
let element = document.createElement("div");
element.style.setProperty("width", "-100");
assert_false(element.hasAttribute("style"));
}, "Setting an invalid value via the declaration setter doesn't create a declaration");
</script>

View File

@ -189,6 +189,7 @@ class FirefoxBrowser(Browser):
self.asan = asan
self.lsan_allowed = None
self.lsan_max_stack_depth = None
self.leak_check = leak_check
self.leak_report_file = None
self.lsan_handler = None
@ -198,6 +199,7 @@ class FirefoxBrowser(Browser):
def settings(self, test):
self.lsan_allowed = test.lsan_allowed
self.lsan_max_stack_depth = test.lsan_max_stack_depth
return {"check_leaks": self.leak_check and not test.leaks,
"lsan_allowed": test.lsan_allowed}
@ -213,7 +215,8 @@ class FirefoxBrowser(Browser):
print "Setting up LSAN"
self.lsan_handler = mozleak.LSANLeaks(self.logger,
scope=group_metadata.get("scope", "/"),
allowed=self.lsan_allowed)
allowed=self.lsan_allowed,
maxNumRecordedFrames=self.lsan_max_stack_depth)
env = test_environment(xrePath=os.path.dirname(self.binary),
debugger=self.debug_info is not None,

View File

@ -156,6 +156,10 @@ class ExpectedManifest(ManifestItem):
def lsan_allowed(self):
return lsan_allowed(self)
@property
def lsan_max_stack_depth(self):
return int_prop("lsan-max-stack-depth", self)
class DirectoryManifest(ManifestItem):
@property
@ -190,6 +194,9 @@ class DirectoryManifest(ManifestItem):
def lsan_allowed(self):
return lsan_allowed(self)
@property
def lsan_max_stack_depth(self):
return int_prop("lsan-max-stack-depth", self)
class TestNode(ManifestItem):
def __init__(self, name):
@ -251,6 +258,10 @@ class TestNode(ManifestItem):
def lsan_allowed(self):
return lsan_allowed(self)
@property
def lsan_max_stack_depth(self):
return int_prop("lsan-max-stack-depth", self)
def append(self, node):
"""Add a subtest to the current test

View File

@ -18,6 +18,10 @@ min-asserts: 1
tags: [b, c]
"""
dir_ini_2 = """\
lsan-max-stack-depth: 42
"""
test_0 = """\
[0.html]
prefs: [c:d]
@ -33,6 +37,11 @@ test_1 = """\
if os == 'win': FAIL
"""
test_2 = """\
[2.html]
lsan-max-stack-depth: 42
"""
def test_metadata_inherit():
tests = make_mock_manifest(("test", "a", 10), ("test", "a/b", 10),
@ -72,3 +81,40 @@ def test_conditional():
test_obj = wpttest.from_manifest(test, [], test_metadata.get_test(test.id))
assert test_obj.prefs == {"a": "b", "c": "d"}
assert test_obj.expected() == "FAIL"
def test_metadata_lsan_stack_depth():
tests = make_mock_manifest(("test", "a", 10), ("test", "a/b", 10))
test_metadata = manifestexpected.static.compile(BytesIO(test_2),
{},
data_cls_getter=manifestexpected.data_cls_getter,
test_path="a",
url_base="")
test = tests[2][2].pop()
test_obj = wpttest.from_manifest(test, [], test_metadata.get_test(test.id))
assert test_obj.lsan_max_stack_depth == 42
test = tests[1][2].pop()
test_obj = wpttest.from_manifest(test, [], test_metadata.get_test(test.id))
assert test_obj.lsan_max_stack_depth == None
test_metadata = manifestexpected.static.compile(BytesIO(test_0),
{},
data_cls_getter=manifestexpected.data_cls_getter,
test_path="a",
url_base="")
inherit_metadata = [
manifestexpected.static.compile(
BytesIO(dir_ini_2),
{},
data_cls_getter=lambda x,y: manifestexpected.DirectoryManifest)
]
test = tests[0][2].pop()
test_obj = wpttest.from_manifest(test, inherit_metadata, test_metadata.get_test(test.id))
assert test_obj.lsan_max_stack_depth == 42

View File

@ -234,6 +234,14 @@ class Test(object):
break
return lsan_allowed
@property
def lsan_max_stack_depth(self):
for meta in self.itermeta(None):
depth = meta.lsan_max_stack_depth
if depth is not None:
return depth
return None
@property
def tags(self):
tags = set()

View File

@ -1868,7 +1868,7 @@ async function placesBookmarkToSyncBookmark(db, bookmarkItem) {
// Converts a Sync bookmark object to a Places bookmark or livemark object.
// This function maps record IDs to Places GUIDs, and filters out extra Sync
// properties like keywords, tags. Returns an object that can be passed to
// `PlacesUtils.livemarks.addLivemark` or `PlacesUtils.bookmarks.{insert, update}`.
// `PlacesUtils.bookmarks.{insert, update}`.
function syncBookmarkToPlacesBookmark(info) {
let bookmarkInfo = {
source: SOURCE_SYNC,

View File

@ -1,516 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Tests functionality of the mozIAsyncLivemarks interface.
const FEED_URI = NetUtil.newURI("http://feed.rss/");
const SITE_URI = NetUtil.newURI("http://site.org/");
let unfiledFolderId;
// This test must be the first one, since it's testing the cache.
add_task(async function test_livemark_cache() {
unfiledFolderId =
await PlacesUtils.promiseItemId(PlacesUtils.bookmarks.unfiledGuid);
// Add a livemark through other APIs.
let folder = await PlacesUtils.bookmarks.insert({
type: PlacesUtils.bookmarks.TYPE_FOLDER,
title: "test",
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
});
let id = await PlacesUtils.promiseItemId(folder.guid);
PlacesUtils.annotations
.setItemAnnotation(id, PlacesUtils.LMANNO_FEEDURI,
"http://example.com/feed",
0, PlacesUtils.annotations.EXPIRE_NEVER);
PlacesUtils.annotations
.setItemAnnotation(id, PlacesUtils.LMANNO_SITEURI,
"http://example.com/site",
0, PlacesUtils.annotations.EXPIRE_NEVER);
let livemark = await PlacesUtils.livemarks.getLivemark({ guid: folder.guid });
Assert.equal(folder.guid, livemark.guid);
Assert.equal(folder.dateAdded * 1000, livemark.dateAdded);
Assert.equal(folder.parentGuid, livemark.parentGuid);
Assert.equal(folder.index, livemark.index);
Assert.equal(folder.title, livemark.title);
Assert.equal(id, livemark.id);
Assert.equal(unfiledFolderId, livemark.parentId);
Assert.equal("http://example.com/feed", livemark.feedURI.spec);
Assert.equal("http://example.com/site", livemark.siteURI.spec);
await PlacesUtils.livemarks.removeLivemark(livemark);
});
add_task(async function test_addLivemark_noArguments_throws() {
try {
await PlacesUtils.livemarks.addLivemark();
do_throw("Invoking addLivemark with no arguments should throw");
} catch (ex) {
Assert.equal(ex.result, Cr.NS_ERROR_XPC_NOT_ENOUGH_ARGS);
}
});
add_task(async function test_addLivemark_emptyObject_throws() {
try {
await PlacesUtils.livemarks.addLivemark({});
do_throw("Invoking addLivemark with empty object should throw");
} catch (ex) {
Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
}
});
add_task(async function test_addLivemark_badParentId_throws() {
try {
await PlacesUtils.livemarks.addLivemark({ parentId: "test" });
do_throw("Invoking addLivemark with a bad parent id should throw");
} catch (ex) {
Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
}
});
add_task(async function test_addLivemark_invalidParentId_throws() {
try {
await PlacesUtils.livemarks.addLivemark({ parentId: -2 });
do_throw("Invoking addLivemark with an invalid parent id should throw");
} catch (ex) {
Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
}
});
add_task(async function test_addLivemark_noIndex_throws() {
try {
await PlacesUtils.livemarks.addLivemark({
parentId: unfiledFolderId });
do_throw("Invoking addLivemark with no index should throw");
} catch (ex) {
Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
}
});
add_task(async function test_addLivemark_badIndex_throws() {
try {
await PlacesUtils.livemarks.addLivemark(
{ parentId: unfiledFolderId,
index: "test" });
do_throw("Invoking addLivemark with a bad index should throw");
} catch (ex) {
Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
}
});
add_task(async function test_addLivemark_invalidIndex_throws() {
try {
await PlacesUtils.livemarks.addLivemark(
{ parentId: unfiledFolderId,
index: -2,
});
do_throw("Invoking addLivemark with an invalid index should throw");
} catch (ex) {
Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
}
});
add_task(async function test_addLivemark_noFeedURI_throws() {
try {
await PlacesUtils.livemarks.addLivemark(
{ parentGuid: PlacesUtils.bookmarks.unfiledGuid });
do_throw("Invoking addLivemark with no feedURI should throw");
} catch (ex) {
Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
}
});
add_task(async function test_addLivemark_badFeedURI_throws() {
try {
await PlacesUtils.livemarks.addLivemark(
{ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
feedURI: "test" });
do_throw("Invoking addLivemark with a bad feedURI should throw");
} catch (ex) {
Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
}
});
add_task(async function test_addLivemark_badSiteURI_throws() {
try {
await PlacesUtils.livemarks.addLivemark(
{ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
feedURI: FEED_URI,
siteURI: "test" });
do_throw("Invoking addLivemark with a bad siteURI should throw");
} catch (ex) {
Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
}
});
add_task(async function test_addLivemark_badGuid_throws() {
try {
await PlacesUtils.livemarks.addLivemark(
{ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
feedURI: FEED_URI,
guid: "123456" });
do_throw("Invoking addLivemark with a bad guid should throw");
} catch (ex) {
Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
}
});
add_task(async function test_addLivemark_parentId_succeeds() {
let onItemAddedCalled = false;
let listener = events => {
Assert.equal(events.length, 1);
let event = events[0];
onItemAddedCalled = true;
PlacesUtils.observers.removeListener(["bookmark-added"], listener);
Assert.equal(event.parentId, unfiledFolderId);
Assert.equal(event.index, 0);
Assert.equal(event.itemType, PlacesUtils.bookmarks.TYPE_FOLDER);
Assert.equal(event.title, "test");
};
PlacesUtils.observers.addListener(["bookmark-added"], listener);
await PlacesUtils.livemarks.addLivemark(
{ title: "test",
parentId: unfiledFolderId,
feedURI: FEED_URI });
Assert.ok(onItemAddedCalled);
});
add_task(async function test_addLivemark_noSiteURI_succeeds() {
let livemark = await PlacesUtils.livemarks.addLivemark(
{ title: "test",
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
feedURI: FEED_URI,
});
Assert.ok(livemark.id > 0);
do_check_valid_places_guid(livemark.guid);
Assert.equal(livemark.title, "test");
Assert.equal(livemark.parentId, unfiledFolderId);
Assert.equal(livemark.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
Assert.ok(livemark.feedURI.equals(FEED_URI));
Assert.equal(livemark.siteURI, null);
Assert.ok(livemark.lastModified > 0);
Assert.equal(livemark.dateAdded, livemark.lastModified);
let bookmark = await PlacesUtils.bookmarks.fetch(livemark.guid);
Assert.equal(livemark.index, bookmark.index);
Assert.equal(livemark.dateAdded, bookmark.dateAdded * 1000);
});
add_task(async function test_addLivemark_succeeds() {
let livemark = await PlacesUtils.livemarks.addLivemark(
{ title: "test",
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
feedURI: FEED_URI,
siteURI: SITE_URI,
});
Assert.ok(livemark.id > 0);
do_check_valid_places_guid(livemark.guid);
Assert.equal(livemark.title, "test");
Assert.equal(livemark.parentId, unfiledFolderId);
Assert.equal(livemark.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
Assert.ok(livemark.feedURI.equals(FEED_URI));
Assert.ok(livemark.siteURI.equals(SITE_URI));
Assert.ok(PlacesUtils.annotations
.itemHasAnnotation(livemark.id,
PlacesUtils.LMANNO_FEEDURI));
Assert.ok(PlacesUtils.annotations
.itemHasAnnotation(livemark.id,
PlacesUtils.LMANNO_SITEURI));
});
add_task(async function test_addLivemark_bogusid_succeeds() {
let livemark = await PlacesUtils.livemarks.addLivemark(
{ id: 100, // Should be ignored.
title: "test",
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
feedURI: FEED_URI,
siteURI: SITE_URI,
});
Assert.ok(livemark.id > 0);
Assert.notEqual(livemark.id, 100);
});
add_task(async function test_addLivemark_bogusParentId_fails() {
try {
await PlacesUtils.livemarks.addLivemark(
{ title: "test",
parentId: 187,
feedURI: FEED_URI,
});
do_throw("Adding a livemark with a bogus parent should fail");
} catch (ex) {}
});
add_task(async function test_addLivemark_bogusParentGuid_fails() {
try {
await PlacesUtils.livemarks.addLivemark(
{ title: "test",
parentGuid: "123456789012",
feedURI: FEED_URI,
});
do_throw("Adding a livemark with a bogus parent should fail");
} catch (ex) {}
});
add_task(async function test_addLivemark_intoLivemark_fails() {
let livemark = await PlacesUtils.livemarks.addLivemark(
{ title: "test",
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
feedURI: FEED_URI,
});
try {
await PlacesUtils.livemarks.addLivemark(
{ title: "test",
parentGuid: livemark.guid,
feedURI: FEED_URI,
});
do_throw("Adding a livemark into a livemark should fail");
} catch (ex) {
Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
}
});
add_task(async function test_addLivemark_forceGuid_succeeds() {
let livemark = await PlacesUtils.livemarks.addLivemark(
{ title: "test",
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
feedURI: FEED_URI,
guid: "1234567890AB",
});
Assert.equal(livemark.guid, "1234567890AB");
do_check_guid_for_bookmark(livemark.id, "1234567890AB");
});
add_task(async function test_addLivemark_dateAdded_succeeds() {
let dateAdded = new Date("2013-03-01T01:10:00") * 1000;
let livemark = await PlacesUtils.livemarks.addLivemark(
{ title: "test",
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
feedURI: FEED_URI,
dateAdded,
});
Assert.equal(livemark.dateAdded, dateAdded);
});
add_task(async function test_addLivemark_lastModified_succeeds() {
let now = Date.now() * 1000;
let livemark = await PlacesUtils.livemarks.addLivemark(
{ title: "test",
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
feedURI: FEED_URI,
lastModified: now,
});
Assert.equal(livemark.dateAdded, now);
Assert.equal(livemark.lastModified, now);
});
add_task(async function test_removeLivemark_emptyObject_throws() {
try {
await PlacesUtils.livemarks.removeLivemark({});
do_throw("Invoking removeLivemark with empty object should throw");
} catch (ex) {
Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
}
});
add_task(async function test_removeLivemark_noValidId_throws() {
try {
await PlacesUtils.livemarks.removeLivemark({ id: -10, guid: "test"});
do_throw("Invoking removeLivemark with no valid id should throw");
} catch (ex) {
Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
}
});
add_task(async function test_removeLivemark_nonExistent_fails() {
try {
await PlacesUtils.livemarks.removeLivemark({ id: 1337 });
do_throw("Removing a non-existent livemark should fail");
} catch (ex) {
}
});
add_task(async function test_removeLivemark_guid_succeeds() {
let livemark = await PlacesUtils.livemarks.addLivemark(
{ title: "test",
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
feedURI: FEED_URI,
guid: "234567890ABC",
});
Assert.equal(livemark.guid, "234567890ABC");
await PlacesUtils.livemarks.removeLivemark({
id: 789, guid: "234567890ABC",
});
Assert.equal((await PlacesUtils.bookmarks.fetch("234567890ABC")), null);
});
add_task(async function test_removeLivemark_id_succeeds() {
let livemark = await PlacesUtils.livemarks.addLivemark(
{ title: "test",
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
feedURI: FEED_URI,
});
await PlacesUtils.livemarks.removeLivemark({ id: livemark.id });
Assert.equal((await PlacesUtils.bookmarks.fetch("234567890ABC")), null);
});
add_task(async function test_getLivemark_emptyObject_throws() {
try {
await PlacesUtils.livemarks.getLivemark({});
do_throw("Invoking getLivemark with empty object should throw");
} catch (ex) {
Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
}
});
add_task(async function test_getLivemark_noValidId_throws() {
try {
await PlacesUtils.livemarks.getLivemark({ id: -10, guid: "test"});
do_throw("Invoking getLivemark with no valid id should throw");
} catch (ex) {
Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
}
});
add_task(async function test_getLivemark_nonExistentId_fails() {
try {
await PlacesUtils.livemarks.getLivemark({ id: 1234 });
do_throw("getLivemark for a non existent id should fail");
} catch (ex) {}
});
add_task(async function test_getLivemark_nonExistentGUID_fails() {
try {
await PlacesUtils.livemarks.getLivemark({ guid: "34567890ABCD" });
do_throw("getLivemark for a non-existent guid should fail");
} catch (ex) {}
});
add_task(async function test_getLivemark_guid_succeeds() {
await PlacesUtils.livemarks.addLivemark(
{ title: "test",
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
feedURI: FEED_URI,
guid: "34567890ABCD" });
// invalid id to check the guid wins.
let livemark =
await PlacesUtils.livemarks.getLivemark({ id: 789, guid: "34567890ABCD" });
Assert.equal(livemark.title, "test");
Assert.equal(livemark.parentId, unfiledFolderId);
Assert.equal(livemark.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
Assert.ok(livemark.feedURI.equals(FEED_URI));
Assert.equal(livemark.siteURI, null);
Assert.equal(livemark.guid, "34567890ABCD");
let bookmark = await PlacesUtils.bookmarks.fetch("34567890ABCD");
Assert.equal(livemark.index, bookmark.index);
});
add_task(async function test_getLivemark_id_succeeds() {
let livemark = await PlacesUtils.livemarks.addLivemark(
{ title: "test",
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
feedURI: FEED_URI,
});
livemark = await PlacesUtils.livemarks.getLivemark({ id: livemark.id });
Assert.equal(livemark.title, "test");
Assert.equal(livemark.parentId, unfiledFolderId);
Assert.equal(livemark.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
Assert.ok(livemark.feedURI.equals(FEED_URI));
Assert.equal(livemark.siteURI, null);
do_check_guid_for_bookmark(livemark.id, livemark.guid);
let bookmark = await PlacesUtils.bookmarks.fetch(livemark.guid);
Assert.equal(livemark.index, bookmark.index);
});
add_task(async function test_getLivemark_removeItem_contention() {
// do not yield.
PlacesUtils.livemarks.addLivemark({ title: "test",
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
feedURI: FEED_URI,
}).catch(() => { /* swallow errors*/ });
await PlacesUtils.bookmarks.eraseEverything();
let livemark = await PlacesUtils.livemarks.addLivemark(
{ title: "test",
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
feedURI: FEED_URI,
});
livemark = await PlacesUtils.livemarks.getLivemark({ guid: livemark.guid });
Assert.equal(livemark.title, "test");
Assert.equal(livemark.parentId, unfiledFolderId);
Assert.ok(livemark.feedURI.equals(FEED_URI));
Assert.equal(livemark.siteURI, null);
do_check_guid_for_bookmark(livemark.id, livemark.guid);
});
add_task(async function test_title_change() {
let livemark = await PlacesUtils.livemarks.addLivemark(
{ title: "test",
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
feedURI: FEED_URI,
});
await PlacesUtils.bookmarks.update({ guid: livemark.guid,
title: "test2" });
// Poll for the title change.
while (true) {
let lm = await PlacesUtils.livemarks.getLivemark({ guid: livemark.guid });
if (lm.title == "test2")
break;
await new Promise(resolve => do_timeout(resolve, 100));
}
});
add_task(async function test_livemark_move() {
let livemark = await PlacesUtils.livemarks.addLivemark(
{ title: "test",
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
feedURI: FEED_URI } );
await PlacesUtils.bookmarks.update({ guid: livemark.guid,
parentGuid: PlacesUtils.bookmarks.toolbarGuid,
index: PlacesUtils.bookmarks.DEFAULT_INDEX });
// Poll for the parent change.
while (true) {
let lm = await PlacesUtils.livemarks.getLivemark({ guid: livemark.guid });
if (lm.parentGuid == PlacesUtils.bookmarks.toolbarGuid)
break;
await new Promise(resolve => do_timeout(resolve, 100));
}
});
add_task(async function test_livemark_removed() {
let livemark = await PlacesUtils.livemarks.addLivemark(
{ title: "test",
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
feedURI: FEED_URI } );
await PlacesUtils.bookmarks.remove(livemark.guid);
// Poll for the livemark removal.
while (true) {
try {
await PlacesUtils.livemarks.getLivemark({ guid: livemark.guid });
} catch (ex) {
break;
}
await new Promise(resolve => do_timeout(resolve, 100));
}
});