зеркало из https://github.com/mozilla/gecko-dev.git
Merge autoland to central, a=merge
This commit is contained in:
Коммит
0dc89fa8e1
|
@ -0,0 +1,44 @@
|
|||
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
"use strict";
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "SessionStore",
|
||||
"resource:///modules/sessionstore/SessionStore.jsm");
|
||||
|
||||
function getRecentlyClosed(maxResults, extension) {
|
||||
let recentlyClosed = [];
|
||||
|
||||
// Get closed windows
|
||||
let closedWindowData = SessionStore.getClosedWindowData(false);
|
||||
for (let window of closedWindowData) {
|
||||
recentlyClosed.push({
|
||||
lastModified: window.closedAt,
|
||||
window: WindowManager.convertFromSessionStoreClosedData(window, extension)});
|
||||
}
|
||||
|
||||
// Get closed tabs
|
||||
for (let window of WindowListManager.browserWindows()) {
|
||||
let closedTabData = SessionStore.getClosedTabData(window, false);
|
||||
for (let tab of closedTabData) {
|
||||
recentlyClosed.push({
|
||||
lastModified: tab.closedAt,
|
||||
tab: TabManager.for(extension).convertFromSessionStoreClosedData(tab, window)});
|
||||
}
|
||||
}
|
||||
|
||||
// Sort windows and tabs
|
||||
recentlyClosed.sort((a, b) => b.lastModified - a.lastModified);
|
||||
return recentlyClosed.slice(0, maxResults);
|
||||
}
|
||||
|
||||
extensions.registerSchemaAPI("sessions", "addon_parent", context => {
|
||||
let {extension} = context;
|
||||
return {
|
||||
sessions: {
|
||||
getRecentlyClosed: function(filter) {
|
||||
let maxResults = filter.maxResults == undefined ? this.MAX_SESSION_RESULTS : filter.maxResults;
|
||||
return Promise.resolve(getRecentlyClosed(maxResults, extension));
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
|
@ -665,6 +665,23 @@ ExtensionTabManager.prototype = {
|
|||
return result;
|
||||
},
|
||||
|
||||
// Converts tabs returned from SessionStore.getClosedTabData and
|
||||
// SessionStore.getClosedWindowData into API tab objects
|
||||
convertFromSessionStoreClosedData(tab, window) {
|
||||
let result = {
|
||||
sessionId: String(tab.closedId),
|
||||
index: tab.pos ? tab.pos : 0,
|
||||
windowId: WindowManager.getId(window),
|
||||
selected: false,
|
||||
highlighted: false,
|
||||
active: false,
|
||||
pinned: false,
|
||||
incognito: Boolean(tab.state && tab.state.isPrivate),
|
||||
};
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
getTabs(window) {
|
||||
return Array.from(window.gBrowser.tabs)
|
||||
.filter(tab => !tab.closing)
|
||||
|
@ -912,6 +929,19 @@ global.WindowManager = {
|
|||
return null;
|
||||
},
|
||||
|
||||
getState(window) {
|
||||
const STATES = {
|
||||
[window.STATE_MAXIMIZED]: "maximized",
|
||||
[window.STATE_MINIMIZED]: "minimized",
|
||||
[window.STATE_NORMAL]: "normal",
|
||||
};
|
||||
let state = STATES[window.windowState];
|
||||
if (window.fullScreen) {
|
||||
state = "fullscreen";
|
||||
}
|
||||
return state;
|
||||
},
|
||||
|
||||
setState(window, state) {
|
||||
if (state != "fullscreen" && window.fullScreen) {
|
||||
window.fullScreen = false;
|
||||
|
@ -952,16 +982,6 @@ global.WindowManager = {
|
|||
},
|
||||
|
||||
convert(extension, window, getInfo) {
|
||||
const STATES = {
|
||||
[window.STATE_MAXIMIZED]: "maximized",
|
||||
[window.STATE_MINIMIZED]: "minimized",
|
||||
[window.STATE_NORMAL]: "normal",
|
||||
};
|
||||
let state = STATES[window.windowState];
|
||||
if (window.fullScreen) {
|
||||
state = "fullscreen";
|
||||
}
|
||||
|
||||
let xulWindow = window.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDocShell)
|
||||
.treeOwner.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
|
@ -976,7 +996,7 @@ global.WindowManager = {
|
|||
height: window.outerHeight,
|
||||
incognito: PrivateBrowsingUtils.isWindowPrivate(window),
|
||||
type: this.windowType(window),
|
||||
state,
|
||||
state: this.getState(window),
|
||||
alwaysOnTop: xulWindow.zLevel >= Ci.nsIXULWindow.raisedZ,
|
||||
};
|
||||
|
||||
|
@ -986,6 +1006,28 @@ global.WindowManager = {
|
|||
|
||||
return result;
|
||||
},
|
||||
|
||||
// Converts windows returned from SessionStore.getClosedWindowData
|
||||
// into API window objects
|
||||
convertFromSessionStoreClosedData(window, extension) {
|
||||
let result = {
|
||||
sessionId: String(window.closedId),
|
||||
focused: false,
|
||||
incognito: false,
|
||||
type: "normal", // this is always "normal" for a closed window
|
||||
state: this.getState(window),
|
||||
alwaysOnTop: false,
|
||||
};
|
||||
|
||||
if (window.tabs.length) {
|
||||
result.tabs = [];
|
||||
window.tabs.forEach((tab, index) => {
|
||||
result.tabs.push(TabManager.for(extension).convertFromSessionStoreClosedData(tab, window, index));
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
};
|
||||
|
||||
// Manages listeners for window opening and closing. A window is
|
||||
|
|
|
@ -6,6 +6,7 @@ category webextension-scripts contextMenus chrome://browser/content/ext-contextM
|
|||
category webextension-scripts desktop-runtime chrome://browser/content/ext-desktop-runtime.js
|
||||
category webextension-scripts history chrome://browser/content/ext-history.js
|
||||
category webextension-scripts pageAction chrome://browser/content/ext-pageAction.js
|
||||
category webextension-scripts sessions chrome://browser/content/ext-sessions.js
|
||||
category webextension-scripts tabs chrome://browser/content/ext-tabs.js
|
||||
category webextension-scripts utils chrome://browser/content/ext-utils.js
|
||||
category webextension-scripts windows chrome://browser/content/ext-windows.js
|
||||
|
@ -24,5 +25,6 @@ category webextension-schemas context_menus chrome://browser/content/schemas/con
|
|||
category webextension-schemas context_menus_internal chrome://browser/content/schemas/context_menus_internal.json
|
||||
category webextension-schemas history chrome://browser/content/schemas/history.json
|
||||
category webextension-schemas page_action chrome://browser/content/schemas/page_action.json
|
||||
category webextension-schemas sessions chrome://browser/content/schemas/sessions.json
|
||||
category webextension-schemas tabs chrome://browser/content/schemas/tabs.json
|
||||
category webextension-schemas windows chrome://browser/content/schemas/windows.json
|
||||
|
|
|
@ -19,6 +19,7 @@ browser.jar:
|
|||
content/browser/ext-desktop-runtime.js
|
||||
content/browser/ext-history.js
|
||||
content/browser/ext-pageAction.js
|
||||
content/browser/ext-sessions.js
|
||||
content/browser/ext-tabs.js
|
||||
content/browser/ext-utils.js
|
||||
content/browser/ext-windows.js
|
||||
|
|
|
@ -10,5 +10,6 @@ browser.jar:
|
|||
content/browser/schemas/context_menus_internal.json
|
||||
content/browser/schemas/history.json
|
||||
content/browser/schemas/page_action.json
|
||||
content/browser/schemas/sessions.json
|
||||
content/browser/schemas/tabs.json
|
||||
content/browser/schemas/windows.json
|
||||
|
|
|
@ -0,0 +1,147 @@
|
|||
// Copyright 2013 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
[
|
||||
{
|
||||
"namespace": "manifest",
|
||||
"types": [
|
||||
{
|
||||
"$extend": "Permission",
|
||||
"choices": [{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"sessions"
|
||||
]
|
||||
}]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"namespace": "sessions",
|
||||
"description": "Use the <code>chrome.sessions</code> API to query and restore tabs and windows from a browsing session.",
|
||||
"permissions": ["sessions"],
|
||||
"types": [
|
||||
{
|
||||
"id": "Filter",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"maxResults": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 25,
|
||||
"optional": true,
|
||||
"description": "The maximum number of entries to be fetched in the requested list. Omit this parameter to fetch the maximum number of entries ($(ref:sessions.MAX_SESSION_RESULTS))."
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "Session",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"lastModified": {"type": "integer", "description": "The time when the window or tab was closed or modified, represented in milliseconds since the epoch."},
|
||||
"tab": {"$ref": "tabs.Tab", "optional": true, "description": "The $(ref:tabs.Tab), if this entry describes a tab. Either this or $(ref:sessions.Session.window) will be set."},
|
||||
"window": {"$ref": "windows.Window", "optional": true, "description": "The $(ref:windows.Window), if this entry describes a window. Either this or $(ref:sessions.Session.tab) will be set."}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "Device",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"info": {"type": "string"},
|
||||
"deviceName": {"type": "string", "description": "The name of the foreign device."},
|
||||
"sessions": {"type": "array", "items": {"$ref": "Session"}, "description": "A list of open window sessions for the foreign device, sorted from most recently to least recently modified session."}
|
||||
}
|
||||
}
|
||||
],
|
||||
"functions": [
|
||||
{
|
||||
"name": "getRecentlyClosed",
|
||||
"type": "function",
|
||||
"description": "Gets the list of recently closed tabs and/or windows.",
|
||||
"async": "callback",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "Filter",
|
||||
"name": "filter",
|
||||
"optional": true,
|
||||
"default": {}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "callback",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "sessions", "type": "array", "items": { "$ref": "Session" }, "description": "The list of closed entries in reverse order that they were closed (the most recently closed tab or window will be at index <code>0</code>). The entries may contain either tabs or windows."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "getDevices",
|
||||
"unsupported": true,
|
||||
"type": "function",
|
||||
"description": "Retrieves all devices with synced sessions.",
|
||||
"async": "callback",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "Filter",
|
||||
"name": "filter",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "callback",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "devices", "type": "array", "items": { "$ref": "Device" }, "description": "The list of $(ref:sessions.Device) objects for each synced session, sorted in order from device with most recently modified session to device with least recently modified session. $(ref:tabs.Tab) objects are sorted by recency in the $(ref:windows.Window) of the $(ref:sessions.Session) objects."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "restore",
|
||||
"unsupported": true,
|
||||
"type": "function",
|
||||
"description": "Reopens a $(ref:windows.Window) or $(ref:tabs.Tab), with an optional callback to run when the entry has been restored.",
|
||||
"async": "callback",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"name": "sessionId",
|
||||
"optional": true,
|
||||
"description": "The $(ref:windows.Window.sessionId), or $(ref:tabs.Tab.sessionId) to restore. If this parameter is not specified, the most recently closed session is restored."
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "callback",
|
||||
"optional": true,
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "Session",
|
||||
"name": "restoredSession",
|
||||
"description": "A $(ref:sessions.Session) containing the restored $(ref:windows.Window) or $(ref:tabs.Tab) object."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"events": [
|
||||
{
|
||||
"name": "onChanged",
|
||||
"unsupported": true,
|
||||
"description": "Fired when recently closed tabs and/or windows are changed. This event does not monitor synced sessions changes.",
|
||||
"type": "function"
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"MAX_SESSION_RESULTS": {
|
||||
"value": 25,
|
||||
"description": "The maximum number of $(ref:sessions.Session) that will be included in a requested list."
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
|
@ -78,7 +78,6 @@
|
|||
"description": "Whether the window is set to be always on top."
|
||||
},
|
||||
"sessionId": {
|
||||
"unsupported": true,
|
||||
"type": "string",
|
||||
"optional": true,
|
||||
"description": "The session ID used to uniquely identify a Window obtained from the $(ref:sessions) API."
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
support-files =
|
||||
head.js
|
||||
head_pageAction.js
|
||||
head_sessions.js
|
||||
context.html
|
||||
ctxmenu-image.png
|
||||
context_tabs_onUpdated_page.html
|
||||
|
@ -58,6 +59,8 @@ tags = webextensions
|
|||
[browser_ext_runtime_openOptionsPage.js]
|
||||
[browser_ext_runtime_openOptionsPage_uninstall.js]
|
||||
[browser_ext_runtime_setUninstallURL.js]
|
||||
[browser_ext_sessions_getRecentlyClosed.js]
|
||||
[browser_ext_sessions_getRecentlyClosed_private.js]
|
||||
[browser_ext_simple.js]
|
||||
[browser_ext_tab_runtimeConnect.js]
|
||||
[browser_ext_tabs_audio.js]
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
"use strict";
|
||||
|
||||
/* globals recordInitialTimestamps, onlyNewItemsFilter, checkRecentlyClosed */
|
||||
|
||||
SimpleTest.requestCompleteLog();
|
||||
|
||||
Services.scriptloader.loadSubScript(new URL("head_sessions.js", gTestPath).href,
|
||||
this);
|
||||
|
||||
add_task(function* test_sessions_get_recently_closed() {
|
||||
function* openAndCloseWindow(url = "http://example.com", tabUrls) {
|
||||
let win = yield BrowserTestUtils.openNewBrowserWindow();
|
||||
yield BrowserTestUtils.loadURI(win.gBrowser.selectedBrowser, url);
|
||||
yield BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
|
||||
if (tabUrls) {
|
||||
for (let url of tabUrls) {
|
||||
yield BrowserTestUtils.openNewForegroundTab(win.gBrowser, url);
|
||||
}
|
||||
}
|
||||
yield BrowserTestUtils.closeWindow(win);
|
||||
}
|
||||
|
||||
function background() {
|
||||
Promise.all([
|
||||
browser.sessions.getRecentlyClosed(),
|
||||
browser.tabs.query({active: true, currentWindow: true}),
|
||||
]).then(([recentlyClosed, tabs]) => {
|
||||
browser.test.sendMessage("initialData", {recentlyClosed, currentWindowId: tabs[0].windowId});
|
||||
});
|
||||
|
||||
browser.test.onMessage.addListener((msg, filter) => {
|
||||
if (msg == "check-sessions") {
|
||||
browser.sessions.getRecentlyClosed(filter).then(recentlyClosed => {
|
||||
browser.test.sendMessage("recentlyClosed", recentlyClosed);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
permissions: ["sessions", "tabs"],
|
||||
},
|
||||
background,
|
||||
});
|
||||
|
||||
// Open and close a window that will be ignored, to prove that we are removing previous entries
|
||||
yield openAndCloseWindow();
|
||||
|
||||
yield extension.startup();
|
||||
|
||||
let {recentlyClosed, currentWindowId} = yield extension.awaitMessage("initialData");
|
||||
recordInitialTimestamps(recentlyClosed.map(item => item.lastModified));
|
||||
|
||||
yield openAndCloseWindow();
|
||||
extension.sendMessage("check-sessions");
|
||||
recentlyClosed = yield extension.awaitMessage("recentlyClosed");
|
||||
checkRecentlyClosed(recentlyClosed.filter(onlyNewItemsFilter), 1, currentWindowId);
|
||||
|
||||
yield openAndCloseWindow("about:config", ["about:robots", "about:mozilla"]);
|
||||
extension.sendMessage("check-sessions");
|
||||
recentlyClosed = yield extension.awaitMessage("recentlyClosed");
|
||||
// Check for multiple tabs in most recently closed window
|
||||
is(recentlyClosed[0].window.tabs.length, 3, "most recently closed window has the expected number of tabs");
|
||||
|
||||
let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com");
|
||||
yield BrowserTestUtils.removeTab(tab);
|
||||
|
||||
tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com");
|
||||
yield BrowserTestUtils.removeTab(tab);
|
||||
|
||||
yield openAndCloseWindow();
|
||||
extension.sendMessage("check-sessions");
|
||||
recentlyClosed = yield extension.awaitMessage("recentlyClosed");
|
||||
let finalResult = recentlyClosed.filter(onlyNewItemsFilter);
|
||||
checkRecentlyClosed(finalResult, 5, currentWindowId);
|
||||
|
||||
isnot(finalResult[0].window, undefined, "first item is a window");
|
||||
is(finalResult[0].tab, undefined, "first item is not a tab");
|
||||
isnot(finalResult[1].tab, undefined, "second item is a tab");
|
||||
is(finalResult[1].window, undefined, "second item is not a window");
|
||||
isnot(finalResult[2].tab, undefined, "third item is a tab");
|
||||
is(finalResult[2].window, undefined, "third item is not a window");
|
||||
isnot(finalResult[3].window, undefined, "fourth item is a window");
|
||||
is(finalResult[3].tab, undefined, "fourth item is not a tab");
|
||||
isnot(finalResult[4].window, undefined, "fifth item is a window");
|
||||
is(finalResult[4].tab, undefined, "fifth item is not a tab");
|
||||
|
||||
// test with filter
|
||||
extension.sendMessage("check-sessions", {maxResults: 2});
|
||||
recentlyClosed = yield extension.awaitMessage("recentlyClosed");
|
||||
checkRecentlyClosed(recentlyClosed.filter(onlyNewItemsFilter), 2, currentWindowId);
|
||||
|
||||
yield extension.unload();
|
||||
});
|
|
@ -0,0 +1,61 @@
|
|||
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
"use strict";
|
||||
|
||||
/* globals recordInitialTimestamps, onlyNewItemsFilter, checkRecentlyClosed */
|
||||
|
||||
SimpleTest.requestCompleteLog();
|
||||
|
||||
Services.scriptloader.loadSubScript(new URL("head_sessions.js", gTestPath).href,
|
||||
this);
|
||||
|
||||
add_task(function* test_sessions_get_recently_closed_private() {
|
||||
function background() {
|
||||
browser.test.onMessage.addListener((msg, filter) => {
|
||||
if (msg == "check-sessions") {
|
||||
browser.sessions.getRecentlyClosed(filter).then(recentlyClosed => {
|
||||
browser.test.sendMessage("recentlyClosed", recentlyClosed);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
permissions: ["sessions", "tabs"],
|
||||
},
|
||||
background,
|
||||
});
|
||||
|
||||
// Open a private browsing window.
|
||||
let privateWin = yield BrowserTestUtils.openNewBrowserWindow({private: true});
|
||||
|
||||
yield extension.startup();
|
||||
|
||||
let {Management: {global: {WindowManager}}} = Cu.import("resource://gre/modules/Extension.jsm", {});
|
||||
let privateWinId = WindowManager.getId(privateWin);
|
||||
|
||||
extension.sendMessage("check-sessions");
|
||||
let recentlyClosed = yield extension.awaitMessage("recentlyClosed");
|
||||
recordInitialTimestamps(recentlyClosed.map(item => item.lastModified));
|
||||
|
||||
// Open and close two tabs in the private window
|
||||
let tab = yield BrowserTestUtils.openNewForegroundTab(privateWin.gBrowser, "http://example.com");
|
||||
yield BrowserTestUtils.removeTab(tab);
|
||||
|
||||
tab = yield BrowserTestUtils.openNewForegroundTab(privateWin.gBrowser, "http://example.com");
|
||||
yield BrowserTestUtils.removeTab(tab);
|
||||
|
||||
extension.sendMessage("check-sessions");
|
||||
recentlyClosed = yield extension.awaitMessage("recentlyClosed");
|
||||
checkRecentlyClosed(recentlyClosed.filter(onlyNewItemsFilter), 2, privateWinId, true);
|
||||
|
||||
// Close the private window.
|
||||
yield BrowserTestUtils.closeWindow(privateWin);
|
||||
|
||||
extension.sendMessage("check-sessions");
|
||||
recentlyClosed = yield extension.awaitMessage("recentlyClosed");
|
||||
is(recentlyClosed.filter(onlyNewItemsFilter).length, 0, "the closed private window info was not found in recently closed data");
|
||||
|
||||
yield extension.unload();
|
||||
});
|
|
@ -0,0 +1,47 @@
|
|||
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
"use strict";
|
||||
|
||||
/* exported recordInitialTimestamps onlyNewItemsFilter checkRecentlyClosed */
|
||||
|
||||
let initialTimestamps = [];
|
||||
|
||||
function recordInitialTimestamps(timestamps) {
|
||||
initialTimestamps = timestamps;
|
||||
}
|
||||
|
||||
function onlyNewItemsFilter(item) {
|
||||
return !initialTimestamps.includes(item.lastModified);
|
||||
}
|
||||
|
||||
function checkWindow(window) {
|
||||
for (let prop of ["focused", "incognito", "alwaysOnTop"]) {
|
||||
is(window[prop], false, `closed window has the expected value for ${prop}`);
|
||||
}
|
||||
for (let prop of ["state", "type"]) {
|
||||
is(window[prop], "normal", `closed window has the expected value for ${prop}`);
|
||||
}
|
||||
}
|
||||
|
||||
function checkTab(tab, windowId, incognito) {
|
||||
for (let prop of ["selected", "highlighted", "active", "pinned"]) {
|
||||
is(tab[prop], false, `closed tab has the expected value for ${prop}`);
|
||||
}
|
||||
is(tab.windowId, windowId, "closed tab has the expected value for windowId");
|
||||
is(tab.incognito, incognito, "closed tab has the expected value for incognito");
|
||||
}
|
||||
|
||||
function checkRecentlyClosed(recentlyClosed, expectedCount, windowId, incognito = false) {
|
||||
let sessionIds = new Set();
|
||||
is(recentlyClosed.length, expectedCount, "the expected number of closed tabs/windows was found");
|
||||
for (let item of recentlyClosed) {
|
||||
if (item.window) {
|
||||
sessionIds.add(item.window.sessionId);
|
||||
checkWindow(item.window);
|
||||
} else if (item.tab) {
|
||||
sessionIds.add(item.tab.sessionId);
|
||||
checkTab(item.tab, windowId, incognito);
|
||||
}
|
||||
}
|
||||
is(sessionIds.size, expectedCount, "each item has a unique sessionId");
|
||||
}
|
|
@ -1022,6 +1022,9 @@ void HTMLMediaElement::AbortExistingLoads()
|
|||
|
||||
void HTMLMediaElement::NoSupportedMediaSourceError(const nsACString& aErrorDetails)
|
||||
{
|
||||
if (mDecoder) {
|
||||
ShutdownDecoder();
|
||||
}
|
||||
mError = new MediaError(this, MEDIA_ERR_SRC_NOT_SUPPORTED, aErrorDetails);
|
||||
ChangeNetworkState(nsIDOMHTMLMediaElement::NETWORK_NO_SOURCE);
|
||||
DispatchAsyncEvent(NS_LITERAL_STRING("error"));
|
||||
|
@ -4413,9 +4416,6 @@ void HTMLMediaElement::FirstFrameLoaded()
|
|||
|
||||
void HTMLMediaElement::NetworkError()
|
||||
{
|
||||
if (mDecoder) {
|
||||
ShutdownDecoder();
|
||||
}
|
||||
if (mReadyState == nsIDOMHTMLMediaElement::HAVE_NOTHING) {
|
||||
NoSupportedMediaSourceError();
|
||||
} else {
|
||||
|
@ -4430,9 +4430,6 @@ void HTMLMediaElement::DecodeError(const MediaResult& aError)
|
|||
const char16_t* params[] = { src.get() };
|
||||
ReportLoadError("MediaLoadDecodeError", params, ArrayLength(params));
|
||||
|
||||
if (mDecoder) {
|
||||
ShutdownDecoder();
|
||||
}
|
||||
AudioTracks()->EmptyTracks();
|
||||
VideoTracks()->EmptyTracks();
|
||||
if (mIsLoadingFromSourceChildren) {
|
||||
|
|
|
@ -896,7 +896,6 @@ MediaDecoder::NetworkError()
|
|||
MOZ_ASSERT(NS_IsMainThread());
|
||||
MOZ_ASSERT(!IsShutdown());
|
||||
mOwner->NetworkError();
|
||||
MOZ_ASSERT(IsShutdown());
|
||||
}
|
||||
|
||||
void
|
||||
|
@ -905,7 +904,6 @@ MediaDecoder::DecodeError(const MediaResult& aError)
|
|||
MOZ_ASSERT(NS_IsMainThread());
|
||||
MOZ_ASSERT(!IsShutdown());
|
||||
mOwner->DecodeError(aError);
|
||||
MOZ_ASSERT(IsShutdown());
|
||||
}
|
||||
|
||||
void
|
||||
|
|
|
@ -89,8 +89,7 @@ MP4Decoder::CanHandleMediaType(const MediaContentType& aType,
|
|||
// the web, as opposed to what we use internally (i.e. what our demuxers
|
||||
// etc output).
|
||||
const bool isMP4Audio = aType.GetMIMEType().EqualsASCII("audio/mp4") ||
|
||||
aType.GetMIMEType().EqualsASCII("audio/x-m4a") ||
|
||||
aType.GetMIMEType().EqualsASCII("audio/opus");
|
||||
aType.GetMIMEType().EqualsASCII("audio/x-m4a");
|
||||
const bool isMP4Video =
|
||||
// On B2G, treat 3GPP as MP4 when Gonk PDM is available.
|
||||
#ifdef MOZ_GONK_MEDIACODEC
|
||||
|
@ -105,7 +104,7 @@ MP4Decoder::CanHandleMediaType(const MediaContentType& aType,
|
|||
|
||||
nsTArray<UniquePtr<TrackInfo>> trackInfos;
|
||||
if (aType.GetCodecs().IsEmpty()) {
|
||||
// No codecs specified. Assume AAC/H.264
|
||||
// No codecs specified. Assume H.264
|
||||
if (isMP4Audio) {
|
||||
trackInfos.AppendElement(
|
||||
CreateTrackInfoWithMIMETypeAndContentTypeExtraParameters(
|
||||
|
@ -136,6 +135,18 @@ MP4Decoder::CanHandleMediaType(const MediaContentType& aType,
|
|||
NS_LITERAL_CSTRING("audio/mpeg"), aType));
|
||||
continue;
|
||||
}
|
||||
if (codec.EqualsLiteral("opus")) {
|
||||
trackInfos.AppendElement(
|
||||
CreateTrackInfoWithMIMETypeAndContentTypeExtraParameters(
|
||||
NS_LITERAL_CSTRING("audio/opus"), aType));
|
||||
continue;
|
||||
}
|
||||
if (codec.EqualsLiteral("flac")) {
|
||||
trackInfos.AppendElement(
|
||||
CreateTrackInfoWithMIMETypeAndContentTypeExtraParameters(
|
||||
NS_LITERAL_CSTRING("audio/flac"), aType));
|
||||
continue;
|
||||
}
|
||||
// Note: Only accept H.264 in a video content type, not in an audio
|
||||
// content type.
|
||||
if (IsWhitelistedH264Codec(codec) && isMP4Video) {
|
||||
|
|
|
@ -729,10 +729,11 @@ DecodedStream::NotifyOutput(int64_t aTime)
|
|||
{
|
||||
AssertOwnerThread();
|
||||
mLastOutputTime = aTime;
|
||||
int64_t currentTime = GetPosition();
|
||||
|
||||
// Remove audio samples that have been played by MSG from the queue.
|
||||
RefPtr<MediaData> a = mAudioQueue.PeekFront();
|
||||
for (; a && a->mTime < aTime;) {
|
||||
for (; a && a->mTime < currentTime;) {
|
||||
RefPtr<MediaData> releaseMe = mAudioQueue.PopFront();
|
||||
a = mAudioQueue.PeekFront();
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ ContainerParser::ContainerParser(const nsACString& aType)
|
|||
|
||||
ContainerParser::~ContainerParser() = default;
|
||||
|
||||
bool
|
||||
MediaResult
|
||||
ContainerParser::IsInitSegmentPresent(MediaByteBuffer* aData)
|
||||
{
|
||||
MSE_DEBUG(ContainerParser, "aLength=%u [%x%x%x%x]",
|
||||
|
@ -47,10 +47,10 @@ ContainerParser::IsInitSegmentPresent(MediaByteBuffer* aData)
|
|||
aData->Length() > 1 ? (*aData)[1] : 0,
|
||||
aData->Length() > 2 ? (*aData)[2] : 0,
|
||||
aData->Length() > 3 ? (*aData)[3] : 0);
|
||||
return false;
|
||||
return NS_ERROR_NOT_AVAILABLE;
|
||||
}
|
||||
|
||||
bool
|
||||
MediaResult
|
||||
ContainerParser::IsMediaSegmentPresent(MediaByteBuffer* aData)
|
||||
{
|
||||
MSE_DEBUG(ContainerParser, "aLength=%u [%x%x%x%x]",
|
||||
|
@ -59,14 +59,14 @@ ContainerParser::IsMediaSegmentPresent(MediaByteBuffer* aData)
|
|||
aData->Length() > 1 ? (*aData)[1] : 0,
|
||||
aData->Length() > 2 ? (*aData)[2] : 0,
|
||||
aData->Length() > 3 ? (*aData)[3] : 0);
|
||||
return false;
|
||||
return NS_ERROR_NOT_AVAILABLE;
|
||||
}
|
||||
|
||||
bool
|
||||
MediaResult
|
||||
ContainerParser::ParseStartAndEndTimestamps(MediaByteBuffer* aData,
|
||||
int64_t& aStart, int64_t& aEnd)
|
||||
{
|
||||
return false;
|
||||
return NS_ERROR_NOT_AVAILABLE;
|
||||
}
|
||||
|
||||
bool
|
||||
|
@ -123,7 +123,7 @@ public:
|
|||
static const unsigned NS_PER_USEC = 1000;
|
||||
static const unsigned USEC_PER_SEC = 1000000;
|
||||
|
||||
bool IsInitSegmentPresent(MediaByteBuffer* aData) override
|
||||
MediaResult IsInitSegmentPresent(MediaByteBuffer* aData) override
|
||||
{
|
||||
ContainerParser::IsInitSegmentPresent(aData);
|
||||
// XXX: This is overly primitive, needs to collect data as it's appended
|
||||
|
@ -140,15 +140,17 @@ public:
|
|||
// 0x1654ae6b // -> One or more Tracks
|
||||
|
||||
// 0x1a45dfa3 // EBML
|
||||
if (aData->Length() >= 4 &&
|
||||
(*aData)[0] == 0x1a && (*aData)[1] == 0x45 && (*aData)[2] == 0xdf &&
|
||||
(*aData)[3] == 0xa3) {
|
||||
return true;
|
||||
if (aData->Length() < 4) {
|
||||
return NS_ERROR_NOT_AVAILABLE;
|
||||
}
|
||||
return false;
|
||||
if ((*aData)[0] == 0x1a && (*aData)[1] == 0x45 && (*aData)[2] == 0xdf &&
|
||||
(*aData)[3] == 0xa3) {
|
||||
return NS_OK;
|
||||
}
|
||||
return MediaResult(NS_ERROR_FAILURE, RESULT_DETAIL("Invalid webm content"));
|
||||
}
|
||||
|
||||
bool IsMediaSegmentPresent(MediaByteBuffer* aData) override
|
||||
MediaResult IsMediaSegmentPresent(MediaByteBuffer* aData) override
|
||||
{
|
||||
ContainerParser::IsMediaSegmentPresent(aData);
|
||||
// XXX: This is overly primitive, needs to collect data as it's appended
|
||||
|
@ -163,26 +165,29 @@ public:
|
|||
// 0x1654ae6b // -> One or more Tracks
|
||||
|
||||
// 0x1f43b675 // Cluster
|
||||
if (aData->Length() >= 4 &&
|
||||
(*aData)[0] == 0x1f && (*aData)[1] == 0x43 && (*aData)[2] == 0xb6 &&
|
||||
if (aData->Length() < 4) {
|
||||
return NS_ERROR_NOT_AVAILABLE;
|
||||
}
|
||||
if ((*aData)[0] == 0x1f && (*aData)[1] == 0x43 && (*aData)[2] == 0xb6 &&
|
||||
(*aData)[3] == 0x75) {
|
||||
return true;
|
||||
return NS_OK;
|
||||
}
|
||||
// 0x1c53bb6b // Cues
|
||||
if (aData->Length() >= 4 &&
|
||||
(*aData)[0] == 0x1c && (*aData)[1] == 0x53 && (*aData)[2] == 0xbb &&
|
||||
if ((*aData)[0] == 0x1c && (*aData)[1] == 0x53 && (*aData)[2] == 0xbb &&
|
||||
(*aData)[3] == 0x6b) {
|
||||
return true;
|
||||
return NS_OK;
|
||||
}
|
||||
return false;
|
||||
return MediaResult(NS_ERROR_FAILURE, RESULT_DETAIL("Invalid webm content"));
|
||||
}
|
||||
|
||||
bool ParseStartAndEndTimestamps(MediaByteBuffer* aData,
|
||||
int64_t& aStart, int64_t& aEnd) override
|
||||
MediaResult ParseStartAndEndTimestamps(MediaByteBuffer* aData,
|
||||
int64_t& aStart,
|
||||
int64_t& aEnd) override
|
||||
{
|
||||
bool initSegment = IsInitSegmentPresent(aData);
|
||||
bool initSegment = NS_SUCCEEDED(IsInitSegmentPresent(aData));
|
||||
|
||||
if (mLastMapping && (initSegment || IsMediaSegmentPresent(aData))) {
|
||||
if (mLastMapping &&
|
||||
(initSegment || NS_SUCCEEDED(IsMediaSegmentPresent(aData)))) {
|
||||
// The last data contained a complete cluster but we can only detect it
|
||||
// now that a new one is starting.
|
||||
// We use mOffset as end position to ensure that any blocks not reported
|
||||
|
@ -191,7 +196,7 @@ public:
|
|||
mOffset);
|
||||
mLastMapping.reset();
|
||||
MSE_DEBUG(WebMContainerParser, "New cluster found at start, ending previous one");
|
||||
return false;
|
||||
return NS_ERROR_NOT_AVAILABLE;
|
||||
}
|
||||
|
||||
if (initSegment) {
|
||||
|
@ -223,7 +228,7 @@ public:
|
|||
MOZ_ASSERT(mParser.mInitEndOffset <= mResource->GetLength());
|
||||
if (!mInitData->SetLength(mParser.mInitEndOffset, fallible)) {
|
||||
// Super unlikely OOM
|
||||
return false;
|
||||
return NS_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
mCompleteInitSegmentRange = MediaByteRange(0, mParser.mInitEndOffset);
|
||||
char* buffer = reinterpret_cast<char*>(mInitData->Elements());
|
||||
|
@ -239,7 +244,7 @@ public:
|
|||
mOffset += aData->Length();
|
||||
|
||||
if (mapping.IsEmpty()) {
|
||||
return false;
|
||||
return NS_ERROR_NOT_AVAILABLE;
|
||||
}
|
||||
|
||||
// Calculate media range for first media segment.
|
||||
|
@ -265,7 +270,7 @@ public:
|
|||
|
||||
if (completeIdx < 0) {
|
||||
mLastMapping.reset();
|
||||
return false;
|
||||
return NS_ERROR_NOT_AVAILABLE;
|
||||
}
|
||||
|
||||
if (mCompleteMediaHeaderRange.IsEmpty()) {
|
||||
|
@ -300,7 +305,7 @@ public:
|
|||
if (!previousMapping && completeIdx + 1u >= mapping.Length()) {
|
||||
// We have no previous nor next block available,
|
||||
// so we can't estimate this block's duration.
|
||||
return false;
|
||||
return NS_ERROR_NOT_AVAILABLE;
|
||||
}
|
||||
|
||||
uint64_t frameDuration = (completeIdx + 1u < mapping.Length())
|
||||
|
@ -314,7 +319,7 @@ public:
|
|||
mapping[completeIdx].mEndOffset, mapping.Length(), completeIdx,
|
||||
mCompleteMediaSegmentRange.mEnd);
|
||||
|
||||
return true;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
int64_t GetRoundingError() override
|
||||
|
@ -337,20 +342,30 @@ public:
|
|||
: ContainerParser(aType)
|
||||
{}
|
||||
|
||||
bool IsInitSegmentPresent(MediaByteBuffer* aData) override
|
||||
MediaResult IsInitSegmentPresent(MediaByteBuffer* aData) override
|
||||
{
|
||||
ContainerParser::IsInitSegmentPresent(aData);
|
||||
// Each MP4 atom has a chunk size and chunk type. The root chunk in an MP4
|
||||
// file is the 'ftyp' atom followed by a file type. We just check for a
|
||||
// vaguely valid 'ftyp' atom.
|
||||
AtomParser parser(mType, aData);
|
||||
return parser.StartWithInitSegment();
|
||||
if (!parser.IsValid()) {
|
||||
return MediaResult(
|
||||
NS_ERROR_FAILURE,
|
||||
RESULT_DETAIL("Invalid Box:%s", parser.LastInvalidBox()));
|
||||
}
|
||||
return parser.StartWithInitSegment() ? NS_OK : NS_ERROR_NOT_AVAILABLE;
|
||||
}
|
||||
|
||||
bool IsMediaSegmentPresent(MediaByteBuffer* aData) override
|
||||
MediaResult IsMediaSegmentPresent(MediaByteBuffer* aData) override
|
||||
{
|
||||
AtomParser parser(mType, aData);
|
||||
return parser.StartWithMediaSegment();
|
||||
if (!parser.IsValid()) {
|
||||
return MediaResult(
|
||||
NS_ERROR_FAILURE,
|
||||
RESULT_DETAIL("Invalid Box:%s", parser.LastInvalidBox()));
|
||||
}
|
||||
return parser.StartWithMediaSegment() ? NS_OK : NS_ERROR_NOT_AVAILABLE;
|
||||
}
|
||||
|
||||
private:
|
||||
|
@ -363,13 +378,38 @@ private:
|
|||
mp4_demuxer::AtomType initAtom("ftyp");
|
||||
mp4_demuxer::AtomType mediaAtom("moof");
|
||||
|
||||
// Valid top-level boxes defined in ISO/IEC 14496-12 (Table 1)
|
||||
static const mp4_demuxer::AtomType validBoxes[] = {
|
||||
"ftyp", "moov", // init segment
|
||||
"pdin", "free", "sidx", // optional prior moov box
|
||||
"styp", "moof", "mdat", // media segment
|
||||
"mfra", "skip", "meta", "meco", "ssix", "prft" // others.
|
||||
"pssh", // optional with encrypted EME, though ignored.
|
||||
};
|
||||
|
||||
while (reader.Remaining() >= 8) {
|
||||
uint64_t size = reader.ReadU32();
|
||||
const uint8_t* typec = reader.Peek(4);
|
||||
uint32_t type = reader.ReadU32();
|
||||
mp4_demuxer::AtomType type(reader.ReadU32());
|
||||
MSE_DEBUGV(AtomParser ,"Checking atom:'%c%c%c%c' @ %u",
|
||||
typec[0], typec[1], typec[2], typec[3],
|
||||
(uint32_t)reader.Offset() - 8);
|
||||
|
||||
for (const auto& boxType : validBoxes) {
|
||||
if (type == boxType) {
|
||||
mValid = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!mValid) {
|
||||
// No point continuing.
|
||||
mLastInvalidBox[0] = typec[3];
|
||||
mLastInvalidBox[1] = typec[2];
|
||||
mLastInvalidBox[2] = typec[1];
|
||||
mLastInvalidBox[3] = typec[0];
|
||||
mLastInvalidBox[4] = '\0';
|
||||
break;
|
||||
}
|
||||
if (mInitOffset.isNothing() &&
|
||||
mp4_demuxer::AtomType(type) == initAtom) {
|
||||
mInitOffset = Some(reader.Offset());
|
||||
|
@ -401,26 +441,31 @@ private:
|
|||
}
|
||||
}
|
||||
|
||||
bool StartWithInitSegment()
|
||||
bool StartWithInitSegment() const
|
||||
{
|
||||
return mInitOffset.isSome() &&
|
||||
(mMediaOffset.isNothing() || mInitOffset.ref() < mMediaOffset.ref());
|
||||
}
|
||||
bool StartWithMediaSegment()
|
||||
bool StartWithMediaSegment() const
|
||||
{
|
||||
return mMediaOffset.isSome() &&
|
||||
(mInitOffset.isNothing() || mMediaOffset.ref() < mInitOffset.ref());
|
||||
}
|
||||
bool IsValid() const { return mValid; }
|
||||
const char* LastInvalidBox() const { return mLastInvalidBox; }
|
||||
private:
|
||||
Maybe<size_t> mInitOffset;
|
||||
Maybe<size_t> mMediaOffset;
|
||||
bool mValid = false;
|
||||
char mLastInvalidBox[5];
|
||||
};
|
||||
|
||||
public:
|
||||
bool ParseStartAndEndTimestamps(MediaByteBuffer* aData,
|
||||
int64_t& aStart, int64_t& aEnd) override
|
||||
MediaResult ParseStartAndEndTimestamps(MediaByteBuffer* aData,
|
||||
int64_t& aStart,
|
||||
int64_t& aEnd) override
|
||||
{
|
||||
bool initSegment = IsInitSegmentPresent(aData);
|
||||
bool initSegment = NS_SUCCEEDED(IsInitSegmentPresent(aData));
|
||||
if (initSegment) {
|
||||
mResource = new SourceBufferResource(NS_LITERAL_CSTRING("video/mp4"));
|
||||
mStream = new MP4Stream(mResource);
|
||||
|
@ -431,7 +476,7 @@ public:
|
|||
mParser = new mp4_demuxer::MoofParser(mStream, 0, /* aIsAudio = */ false);
|
||||
mInitData = new MediaByteBuffer();
|
||||
} else if (!mStream || !mParser) {
|
||||
return false;
|
||||
return NS_ERROR_NOT_AVAILABLE;
|
||||
}
|
||||
|
||||
mResource->AppendData(aData);
|
||||
|
@ -446,7 +491,7 @@ public:
|
|||
mCompleteInitSegmentRange = range;
|
||||
if (!mInitData->SetLength(range.Length(), fallible)) {
|
||||
// Super unlikely OOM
|
||||
return false;
|
||||
return NS_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
char* buffer = reinterpret_cast<char*>(mInitData->Elements());
|
||||
mResource->ReadFromCache(buffer, range.mStart, range.Length());
|
||||
|
@ -469,17 +514,17 @@ public:
|
|||
}
|
||||
if (NS_WARN_IF(rv.Failed())) {
|
||||
rv.SuppressException();
|
||||
return false;
|
||||
return NS_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
if (compositionRange.IsNull()) {
|
||||
return false;
|
||||
return NS_ERROR_NOT_AVAILABLE;
|
||||
}
|
||||
aStart = compositionRange.start;
|
||||
aEnd = compositionRange.end;
|
||||
MSE_DEBUG(MP4ContainerParser, "[%lld, %lld]",
|
||||
aStart, aEnd);
|
||||
return true;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// Gaps of up to 35ms (marginally longer than a single frame at 30fps) are considered
|
||||
|
@ -554,24 +599,24 @@ public:
|
|||
return true;
|
||||
}
|
||||
|
||||
bool IsInitSegmentPresent(MediaByteBuffer* aData) override
|
||||
MediaResult IsInitSegmentPresent(MediaByteBuffer* aData) override
|
||||
{
|
||||
// Call superclass for logging.
|
||||
ContainerParser::IsInitSegmentPresent(aData);
|
||||
|
||||
Header header;
|
||||
if (!Parse(aData, header)) {
|
||||
return false;
|
||||
return NS_ERROR_NOT_AVAILABLE;
|
||||
}
|
||||
|
||||
MSE_DEBUGV(ADTSContainerParser, "%llu byte frame %d aac frames%s",
|
||||
(unsigned long long)header.frame_length, (int)header.aac_frames,
|
||||
header.have_crc ? " crc" : "");
|
||||
|
||||
return true;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
bool IsMediaSegmentPresent(MediaByteBuffer* aData) override
|
||||
MediaResult IsMediaSegmentPresent(MediaByteBuffer* aData) override
|
||||
{
|
||||
// Call superclass for logging.
|
||||
ContainerParser::IsMediaSegmentPresent(aData);
|
||||
|
@ -583,26 +628,27 @@ public:
|
|||
// mInitData if we need to handle separate media segments.
|
||||
Header header;
|
||||
if (!Parse(aData, header)) {
|
||||
return false;
|
||||
return NS_ERROR_NOT_AVAILABLE;
|
||||
}
|
||||
// We're supposed to return true as long as aData contains the
|
||||
// start of a media segment, whether or not it's complete. So
|
||||
// return true if we have any data beyond the header.
|
||||
if (aData->Length() <= header.header_length) {
|
||||
return false;
|
||||
return NS_ERROR_NOT_AVAILABLE;
|
||||
}
|
||||
|
||||
// We should have at least a partial frame.
|
||||
return true;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
bool ParseStartAndEndTimestamps(MediaByteBuffer* aData,
|
||||
int64_t& aStart, int64_t& aEnd) override
|
||||
MediaResult ParseStartAndEndTimestamps(MediaByteBuffer* aData,
|
||||
int64_t& aStart,
|
||||
int64_t& aEnd) override
|
||||
{
|
||||
// ADTS header.
|
||||
Header header;
|
||||
if (!Parse(aData, header)) {
|
||||
return false;
|
||||
return NS_ERROR_NOT_AVAILABLE;
|
||||
}
|
||||
mHasInitData = true;
|
||||
mCompleteInitSegmentRange = MediaByteRange(0, int64_t(header.header_length));
|
||||
|
@ -617,7 +663,7 @@ public:
|
|||
" in %llu byte buffer.",
|
||||
(unsigned long long)header.frame_length,
|
||||
(unsigned long long)(aData->Length()));
|
||||
return false;
|
||||
return NS_ERROR_NOT_AVAILABLE;
|
||||
}
|
||||
mCompleteMediaSegmentRange = MediaByteRange(header.header_length,
|
||||
header.frame_length);
|
||||
|
@ -629,7 +675,7 @@ public:
|
|||
MSE_DEBUG(ADTSContainerParser, "[%lld, %lld]",
|
||||
aStart, aEnd);
|
||||
// We don't update timestamps, regardless.
|
||||
return false;
|
||||
return NS_ERROR_NOT_AVAILABLE;
|
||||
}
|
||||
|
||||
// Audio shouldn't have gaps.
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
#include "mozilla/RefPtr.h"
|
||||
#include "nsString.h"
|
||||
#include "MediaResource.h"
|
||||
#include "MediaResult.h"
|
||||
|
||||
namespace mozilla {
|
||||
|
||||
|
@ -24,17 +25,24 @@ public:
|
|||
// Return true if aData starts with an initialization segment.
|
||||
// The base implementation exists only for debug logging and is expected
|
||||
// to be called first from the overriding implementation.
|
||||
virtual bool IsInitSegmentPresent(MediaByteBuffer* aData);
|
||||
// Return NS_OK if segment is present, NS_ERROR_NOT_AVAILABLE if no sufficient
|
||||
// data is currently available to make a determination. Any other value
|
||||
// indicates an error.
|
||||
virtual MediaResult IsInitSegmentPresent(MediaByteBuffer* aData);
|
||||
|
||||
// Return true if aData starts with a media segment.
|
||||
// The base implementation exists only for debug logging and is expected
|
||||
// to be called first from the overriding implementation.
|
||||
virtual bool IsMediaSegmentPresent(MediaByteBuffer* aData);
|
||||
// Return NS_OK if segment is present, NS_ERROR_NOT_AVAILABLE if no sufficient
|
||||
// data is currently available to make a determination. Any other value
|
||||
// indicates an error.
|
||||
virtual MediaResult IsMediaSegmentPresent(MediaByteBuffer* aData);
|
||||
|
||||
// Parse aData to extract the start and end frame times from the media
|
||||
// segment. aData may not start on a parser sync boundary. Return true
|
||||
// if aStart and aEnd have been updated.
|
||||
virtual bool ParseStartAndEndTimestamps(MediaByteBuffer* aData,
|
||||
// segment. aData may not start on a parser sync boundary. Return NS_OK
|
||||
// if aStart and aEnd have been updated and NS_ERROR_NOT_AVAILABLE otherwise
|
||||
// when no error were encountered.
|
||||
virtual MediaResult ParseStartAndEndTimestamps(MediaByteBuffer* aData,
|
||||
int64_t& aStart, int64_t& aEnd);
|
||||
|
||||
// Compare aLhs and rHs, considering any error that may exist in the
|
||||
|
|
|
@ -636,7 +636,8 @@ TrackBuffersManager::SegmentParserLoop()
|
|||
// 4. If the append state equals WAITING_FOR_SEGMENT, then run the following
|
||||
// steps:
|
||||
if (mSourceBufferAttributes->GetAppendState() == AppendState::WAITING_FOR_SEGMENT) {
|
||||
if (mParser->IsInitSegmentPresent(mInputBuffer)) {
|
||||
MediaResult haveInitSegment = mParser->IsInitSegmentPresent(mInputBuffer);
|
||||
if (NS_SUCCEEDED(haveInitSegment)) {
|
||||
SetAppendState(AppendState::PARSING_INIT_SEGMENT);
|
||||
if (mFirstInitializationSegmentReceived) {
|
||||
// This is a new initialization segment. Obsolete the old one.
|
||||
|
@ -644,20 +645,37 @@ TrackBuffersManager::SegmentParserLoop()
|
|||
}
|
||||
continue;
|
||||
}
|
||||
if (mParser->IsMediaSegmentPresent(mInputBuffer)) {
|
||||
MediaResult haveMediaSegment =
|
||||
mParser->IsMediaSegmentPresent(mInputBuffer);
|
||||
if (NS_SUCCEEDED(haveMediaSegment)) {
|
||||
SetAppendState(AppendState::PARSING_MEDIA_SEGMENT);
|
||||
mNewMediaSegmentStarted = true;
|
||||
continue;
|
||||
}
|
||||
// We have neither an init segment nor a media segment, this is either
|
||||
// invalid data or not enough data to detect a segment type.
|
||||
MSE_DEBUG("Found invalid or incomplete data.");
|
||||
// We have neither an init segment nor a media segment.
|
||||
// Check if it was invalid data.
|
||||
if (haveInitSegment != NS_ERROR_NOT_AVAILABLE) {
|
||||
MSE_DEBUG("Found invalid data.");
|
||||
RejectAppend(haveInitSegment, __func__);
|
||||
return;
|
||||
}
|
||||
if (haveMediaSegment != NS_ERROR_NOT_AVAILABLE) {
|
||||
MSE_DEBUG("Found invalid data.");
|
||||
RejectAppend(haveMediaSegment, __func__);
|
||||
return;
|
||||
}
|
||||
MSE_DEBUG("Found incomplete data.");
|
||||
NeedMoreData();
|
||||
return;
|
||||
}
|
||||
|
||||
int64_t start, end;
|
||||
bool newData = mParser->ParseStartAndEndTimestamps(mInputBuffer, start, end);
|
||||
MediaResult newData =
|
||||
mParser->ParseStartAndEndTimestamps(mInputBuffer, start, end);
|
||||
if (!NS_SUCCEEDED(newData) && newData.Code() != NS_ERROR_NOT_AVAILABLE) {
|
||||
RejectAppend(newData, __func__);
|
||||
return;
|
||||
}
|
||||
mProcessedInput += mInputBuffer->Length();
|
||||
|
||||
// 5. If the append state equals PARSING_INIT_SEGMENT, then run the
|
||||
|
@ -684,13 +702,13 @@ TrackBuffersManager::SegmentParserLoop()
|
|||
// If so, recreate a new demuxer to ensure that the demuxer is only fed
|
||||
// monotonically increasing data.
|
||||
if (mNewMediaSegmentStarted) {
|
||||
if (newData && mLastParsedEndTime.isSome() &&
|
||||
if (NS_SUCCEEDED(newData) && mLastParsedEndTime.isSome() &&
|
||||
start < mLastParsedEndTime.ref().ToMicroseconds()) {
|
||||
MSE_DEBUG("Re-creating demuxer");
|
||||
ResetDemuxingState();
|
||||
return;
|
||||
}
|
||||
if (newData || !mParser->MediaSegmentRange().IsEmpty()) {
|
||||
if (NS_SUCCEEDED(newData) || !mParser->MediaSegmentRange().IsEmpty()) {
|
||||
if (mPendingInputBuffer) {
|
||||
// We now have a complete media segment header. We can resume parsing
|
||||
// the data.
|
||||
|
|
|
@ -47,37 +47,37 @@ TEST(ContainerParser, ADTSHeader) {
|
|||
|
||||
// Test a valid header.
|
||||
RefPtr<MediaByteBuffer> header = make_adts_header();
|
||||
EXPECT_TRUE(parser->IsInitSegmentPresent(header));
|
||||
EXPECT_TRUE(NS_SUCCEEDED(parser->IsInitSegmentPresent(header)));
|
||||
|
||||
// Test variations.
|
||||
uint8_t save = header->ElementAt(1);
|
||||
for (uint8_t i = 1; i < 3; ++i) {
|
||||
// Set non-zero layer.
|
||||
header->ReplaceElementAt(1, (header->ElementAt(1) & 0xf9) | (i << 1));
|
||||
EXPECT_FALSE(parser->IsInitSegmentPresent(header))
|
||||
EXPECT_FALSE(NS_SUCCEEDED(parser->IsInitSegmentPresent(header)))
|
||||
<< "Accepted non-zero layer in header.";
|
||||
}
|
||||
header->ReplaceElementAt(1, save);
|
||||
save = header->ElementAt(2);
|
||||
header->ReplaceElementAt(2, (header->ElementAt(2) & 0x3b) | (15 << 2));
|
||||
EXPECT_FALSE(parser->IsInitSegmentPresent(header))
|
||||
EXPECT_FALSE(NS_SUCCEEDED(parser->IsInitSegmentPresent(header)))
|
||||
<< "Accepted explicit frequency in header.";
|
||||
header->ReplaceElementAt(2, save);
|
||||
|
||||
// Test a short header.
|
||||
header->SetLength(6);
|
||||
EXPECT_FALSE(parser->IsInitSegmentPresent(header))
|
||||
EXPECT_FALSE(NS_SUCCEEDED(parser->IsInitSegmentPresent(header)))
|
||||
<< "Accepted too-short header.";
|
||||
EXPECT_FALSE(parser->IsMediaSegmentPresent(header))
|
||||
EXPECT_FALSE(NS_SUCCEEDED(parser->IsMediaSegmentPresent(header)))
|
||||
<< "Found media segment when there was just a partial header.";
|
||||
|
||||
// Test parse results.
|
||||
header = make_adts_header();
|
||||
EXPECT_FALSE(parser->IsMediaSegmentPresent(header))
|
||||
EXPECT_FALSE(NS_SUCCEEDED(parser->IsMediaSegmentPresent(header)))
|
||||
<< "Found media segment when there was just a header.";
|
||||
int64_t start = 0;
|
||||
int64_t end = 0;
|
||||
EXPECT_FALSE(parser->ParseStartAndEndTimestamps(header, start, end));
|
||||
EXPECT_TRUE(NS_FAILED(parser->ParseStartAndEndTimestamps(header, start, end)));
|
||||
|
||||
EXPECT_TRUE(parser->HasInitData());
|
||||
EXPECT_TRUE(parser->HasCompleteInitData());
|
||||
|
|
|
@ -224,8 +224,6 @@ H264Converter::CreateDecoderAndInit(MediaRawData* aSample)
|
|||
// Queue the incoming sample.
|
||||
mMediaRawSamples.AppendElement(aSample);
|
||||
|
||||
RefPtr<H264Converter> self = this;
|
||||
|
||||
mInitPromiseRequest.Begin(mDecoder->Init()
|
||||
->Then(AbstractThread::GetCurrent()->AsTaskQueue(), __func__, this,
|
||||
&H264Converter::OnDecoderInitDone,
|
||||
|
|
|
@ -78,6 +78,16 @@ function check_mp4(v, enabled) {
|
|||
check("audio/x-m4a; codecs=mp4a.40.5", "probably");
|
||||
// HE-AAC v2
|
||||
check("audio/mp4; codecs=\"mp4a.40.29\"", "probably");
|
||||
|
||||
// Opus
|
||||
check("audio/mp4; codecs=\"opus\"", "probably");
|
||||
check("audio/mp4; codecs=opus", "probably");
|
||||
|
||||
// Flac.
|
||||
// Not available on Android yet.
|
||||
var expectedResult = IsSupportedAndroid() ? "" : "probably";
|
||||
check("audio/mp4; codecs=\"flac\"", expectedResult);
|
||||
check("audio/mp4; codecs=flac", expectedResult);
|
||||
}
|
||||
|
||||
function check_mp3(v, enabled) {
|
||||
|
|
|
@ -33,7 +33,7 @@ VIntLength(unsigned char aFirstByte, uint32_t* aMask)
|
|||
return count;
|
||||
}
|
||||
|
||||
void WebMBufferedParser::Append(const unsigned char* aBuffer, uint32_t aLength,
|
||||
bool WebMBufferedParser::Append(const unsigned char* aBuffer, uint32_t aLength,
|
||||
nsTArray<WebMTimeDataOffset>& aMapping,
|
||||
ReentrantMonitor& aReentrantMonitor)
|
||||
{
|
||||
|
@ -163,7 +163,9 @@ void WebMBufferedParser::Append(const unsigned char* aBuffer, uint32_t aLength,
|
|||
}
|
||||
break;
|
||||
case READ_TIMECODESCALE:
|
||||
MOZ_ASSERT(mGotTimecodeScale);
|
||||
if (!mGotTimecodeScale) {
|
||||
return false;
|
||||
}
|
||||
mTimecodeScale = mVInt.mValue;
|
||||
mState = READ_ELEMENT_ID;
|
||||
break;
|
||||
|
@ -187,7 +189,9 @@ void WebMBufferedParser::Append(const unsigned char* aBuffer, uint32_t aLength,
|
|||
if (idx == 0 || aMapping[idx - 1] != endOffset) {
|
||||
// Don't insert invalid negative timecodes.
|
||||
if (mBlockTimecode >= 0 || mClusterTimecode >= uint16_t(abs(mBlockTimecode))) {
|
||||
MOZ_ASSERT(mGotTimecodeScale);
|
||||
if (!mGotTimecodeScale) {
|
||||
return false;
|
||||
}
|
||||
uint64_t absTimecode = mClusterTimecode + mBlockTimecode;
|
||||
absTimecode *= mTimecodeScale;
|
||||
// Avoid creating an entry if the timecode is out of order
|
||||
|
@ -248,6 +252,8 @@ void WebMBufferedParser::Append(const unsigned char* aBuffer, uint32_t aLength,
|
|||
|
||||
NS_ASSERTION(p == aBuffer + aLength, "Must have parsed to end of data.");
|
||||
mCurrentOffset += aLength;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int64_t
|
||||
|
|
|
@ -96,7 +96,8 @@ struct WebMBufferedParser
|
|||
// Steps the parser through aLength bytes of data. Always consumes
|
||||
// aLength bytes. Updates mCurrentOffset before returning. Acquires
|
||||
// aReentrantMonitor before using aMapping.
|
||||
void Append(const unsigned char* aBuffer, uint32_t aLength,
|
||||
// Returns false if an error was encountered.
|
||||
bool Append(const unsigned char* aBuffer, uint32_t aLength,
|
||||
nsTArray<WebMTimeDataOffset>& aMapping,
|
||||
ReentrantMonitor& aReentrantMonitor);
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
[clearkey-mp4-playback-temporary-encrypted-clear.html]
|
||||
type: testharness
|
||||
expected:
|
||||
if not debug and not e10s and (os == "mac") and (version == "OS X 10.10.5") and (processor == "x86") and (bits == 32): ERROR
|
||||
if not debug and e10s and (os == "mac") and (version == "OS X 10.10.5") and (processor == "x86") and (bits == 32): ERROR
|
||||
if debug and not e10s and (os == "mac") and (version == "OS X 10.10.5") and (processor == "x86_64") and (bits == 64): ERROR
|
||||
if debug and e10s and (os == "mac") and (version == "OS X 10.10.5") and (processor == "x86_64") and (bits == 64): ERROR
|
||||
if not debug and not e10s and (os == "mac") and (version == "OS X 10.10.5") and (processor == "x86") and (bits == 32): TIMEOUT
|
||||
if not debug and e10s and (os == "mac") and (version == "OS X 10.10.5") and (processor == "x86") and (bits == 32): TIMEOUT
|
||||
if debug and not e10s and (os == "mac") and (version == "OS X 10.10.5") and (processor == "x86_64") and (bits == 64): TIMEOUT
|
||||
if debug and e10s and (os == "mac") and (version == "OS X 10.10.5") and (processor == "x86_64") and (bits == 64): TIMEOUT
|
||||
[org.w3.clearkey, temporary, mp4, playback, single key, encrypted then clear content]
|
||||
expected:
|
||||
if not debug and e10s and (os == "win") and (version == "5.1.2600") and (processor == "x86") and (bits == 32): TIMEOUT
|
||||
|
|
|
@ -1,18 +1,8 @@
|
|||
[clearkey-mp4-playback-temporary-multisession.html]
|
||||
type: testharness
|
||||
expected:
|
||||
if not debug and not e10s and (os == "mac") and (version == "OS X 10.10.5") and (processor == "x86") and (bits == 32): ERROR
|
||||
if not debug and e10s and (os == "mac") and (version == "OS X 10.10.5") and (processor == "x86") and (bits == 32): ERROR
|
||||
if debug and not e10s and (os == "mac") and (version == "OS X 10.10.5") and (processor == "x86_64") and (bits == 64): ERROR
|
||||
if debug and e10s and (os == "mac") and (version == "OS X 10.10.5") and (processor == "x86_64") and (bits == 64): ERROR
|
||||
[org.w3.clearkey, temporary, mp4, playback with multiple sessions, multikey video]
|
||||
expected:
|
||||
if not debug and e10s and (os == "win") and (version == "5.1.2600") and (processor == "x86") and (bits == 32): TIMEOUT
|
||||
if debug and not e10s and (os == "win") and (version == "5.1.2600") and (processor == "x86") and (bits == 32): TIMEOUT
|
||||
if debug and e10s and (os == "win") and (version == "5.1.2600") and (processor == "x86") and (bits == 32): TIMEOUT
|
||||
if not debug and not e10s and (os == "win") and (version == "5.1.2600") and (processor == "x86") and (bits == 32): TIMEOUT
|
||||
if not debug and not e10s and (os == "mac") and (version == "OS X 10.10.5") and (processor == "x86") and (bits == 32): TIMEOUT
|
||||
if not debug and e10s and (os == "mac") and (version == "OS X 10.10.5") and (processor == "x86") and (bits == 32): TIMEOUT
|
||||
if debug and not e10s and (os == "mac") and (version == "OS X 10.10.5") and (processor == "x86_64") and (bits == 64): TIMEOUT
|
||||
if debug and e10s and (os == "mac") and (version == "OS X 10.10.5") and (processor == "x86_64") and (bits == 64): TIMEOUT
|
||||
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
[mediasource-errors.html]
|
||||
type: testharness
|
||||
expected:
|
||||
if debug and not e10s and (os == "win") and (version == "5.1.2600") and (processor == "x86") and (bits == 32): CRASH
|
||||
if debug and e10s and (os == "win") and (version == "5.1.2600") and (processor == "x86") and (bits == 32): TIMEOUT
|
||||
[Signaling 'decode' error via endOfStream() after initialization segment has been appended and the HTMLMediaElement has reached HAVE_METADATA.]
|
||||
expected: FAIL
|
||||
|
||||
[Signaling 'network' error via endOfStream() after initialization segment has been appended and the HTMLMediaElement has reached HAVE_METADATA.]
|
||||
expected: FAIL
|
||||
|
||||
[Signaling 'decode' error via segment parser loop algorithm after initialization segment and partial media segment has been appended.]
|
||||
expected: FAIL
|
||||
|
|
@ -174,13 +174,49 @@
|
|||
{
|
||||
assert_equals(mediaElement.readyState, HTMLMediaElement.HAVE_NOTHING);
|
||||
|
||||
var initSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.init);
|
||||
test.expectEvent(sourceBuffer, "updateend", "mediaSegment append ended.");
|
||||
test.expectEvent(mediaElement, "loadedmetadata", "mediaElement metadata.");
|
||||
sourceBuffer.appendBuffer(initSegment);
|
||||
|
||||
test.waitForExpectedEvents(function()
|
||||
{
|
||||
assert_equals(mediaElement.readyState, HTMLMediaElement.HAVE_METADATA);
|
||||
var mediaSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.media[0]);
|
||||
var index = segmentInfo.init.size + (mediaSegment.length - 1) / 2;
|
||||
// Corrupt the media data from index of mediaData, so it can signal 'decode' error.
|
||||
// Here use mediaSegment to replace the original mediaData[index, index + mediaSegment.length]
|
||||
mediaData.set(mediaSegment, index);
|
||||
|
||||
test.expectEvent(sourceBuffer, "error", "sourceBuffer error.");
|
||||
test.expectEvent(sourceBuffer, "updateend", "mediaSegment append ended.");
|
||||
test.expectEvent(mediaElement, "error", "mediaElement error.");
|
||||
test.expectEvent(mediaSource, "sourceended", "mediaSource ended.");
|
||||
sourceBuffer.appendBuffer(mediaData);
|
||||
});
|
||||
|
||||
test.waitForExpectedEvents(function()
|
||||
{
|
||||
assert_true(mediaElement.error != null);
|
||||
assert_equals(mediaElement.error.code, MediaError.MEDIA_ERR_DECODE);
|
||||
test.done();
|
||||
});
|
||||
}, "Signaling 'decode' error via segment parser loop algorithm after initialization segment has been appended.");
|
||||
|
||||
ErrorTest(function(test, mediaElement, mediaSource, segmentInfo, sourceBuffer, mediaData)
|
||||
{
|
||||
assert_equals(mediaElement.readyState, HTMLMediaElement.HAVE_NOTHING);
|
||||
|
||||
// Fail if the append error algorithm occurs, since the network
|
||||
// error will be provided by us directly via endOfStream().
|
||||
mediaElement.addEventListener("loadedmetadata", test.unreached_func("'loadedmetadata' should not be fired on mediaElement"));
|
||||
|
||||
var mediaSegment = MediaSourceUtil.extractSegmentData(mediaData, segmentInfo.media[0]);
|
||||
var index = segmentInfo.init.size + (mediaSegment.length - 1) / 2;
|
||||
// Corrupt the media data from index of mediaData, so it can signal 'decode' error.
|
||||
// Here use mediaSegment to replace the original mediaData[index, index + mediaSegment.length]
|
||||
mediaData.set(mediaSegment, index);
|
||||
|
||||
test.expectEvent(mediaElement, "loadedmetadata", "mediaElement metadata.");
|
||||
test.expectEvent(sourceBuffer, "error", "sourceBuffer error.");
|
||||
test.expectEvent(sourceBuffer, "updateend", "mediaSegment append ended.");
|
||||
test.expectEvent(mediaElement, "error", "mediaElement error.");
|
||||
|
@ -189,9 +225,10 @@
|
|||
|
||||
test.waitForExpectedEvents(function()
|
||||
{
|
||||
assert_equals(mediaElement.readyState, HTMLMediaElement.HAVE_NOTHING);
|
||||
assert_true(mediaElement.error != null);
|
||||
assert_equals(mediaElement.error.code, MediaError.MEDIA_ERR_DECODE);
|
||||
assert_equals(mediaElement.error.code, MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED);
|
||||
test.done();
|
||||
});
|
||||
}, "Signaling 'decode' error via segment parser loop algorithm after initialization segment and partial media segment has been appended.");
|
||||
}, "Signaling 'decode' error via segment parser loop algorithm.");
|
||||
</script>
|
||||
|
|
Загрузка…
Ссылка в новой задаче