author | Gregory Szorc <gps@mozilla.com> |
Tue, 05 Feb 2013 22:40:34 -0800 | |
changeset 121005 | bc108d2ce8d17a560e2fc2affa6867ca9418448d |
parent 121004 | 65e0a461f1eff292c87ba4c4336c0571172524c7 (current diff) |
parent 120920 | 8ac096b24cb0ed3d8c3b4a546aac9dc221f36092 (diff) |
child 121006 | ed29092600501b9b2798c6a9c0ee643eed8281ef |
child 121025 | 04e13fc9dbff9391f9a42ab2f084a1f4f13e0729 |
push id | 22524 |
push user | ryanvm@gmail.com |
push date | Wed, 06 Feb 2013 13:23:43 +0000 |
treeherder | mozilla-inbound@470187171cf1 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
milestone | 21.0a1 |
first release with | nightly linux32
bc108d2ce8d1
/
21.0a1
/
20130206031027
/
files
nightly linux64
bc108d2ce8d1
/
21.0a1
/
20130206031027
/
files
nightly mac
bc108d2ce8d1
/
21.0a1
/
20130206031027
/
files
nightly win32
bc108d2ce8d1
/
21.0a1
/
20130206031027
/
files
nightly win64
bc108d2ce8d1
/
21.0a1
/
20130206031027
/
files
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
releases | nightly linux32
21.0a1
/
20130206031027
/
pushlog to previous
nightly linux64
21.0a1
/
20130206031027
/
pushlog to previous
nightly mac
21.0a1
/
20130206031027
/
pushlog to previous
nightly win32
21.0a1
/
20130206031027
/
pushlog to previous
nightly win64
21.0a1
/
20130206031027
/
pushlog to previous
|
content/media/AudioEventTimeline.h | file | annotate | diff | comparison | revisions | |
content/media/webaudio/AudioEventTimeline.h | file | annotate | diff | comparison | revisions | |
docshell/test/bug94514-postpage.html | file | annotate | diff | comparison | revisions | |
docshell/test/test_bug94514.html | file | annotate | diff | comparison | revisions | |
testing/mozbase/README | file | annotate | diff | comparison | revisions | |
toolkit/components/telemetry/Histograms.json | file | annotate | diff | comparison | revisions |
new file mode 100644 index 0000000000000000000000000000000000000000..826e5340843246732f7001c836f4a3ec37681c86 GIT binary patch literal 3409 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
new file mode 100644 index 0000000000000000000000000000000000000000..980e787310a9a1391715ba8d3b9bd9bfbc2b5b5c GIT binary patch literal 3382 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 M07*qoM6N<$f>WkX<^TWy
index 0bd1e60e6a3e2d23f43bfbba3e5c86afa71acf00..6daf7cf7188b00fd28c1c8c3a54673b262036a77 GIT binary patch literal 3217 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 D&kXdV
index e377d321ce03505c9127296eab43b80d3b39f6df..c7837f8229c03de1fcd0c02ef3aa78573e0a69b8 GIT binary patch literal 3042 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
index a8482eb4dd7ebaf168eda6a8ca82e77efeffb930..fd64f969724698d9694b4effceb3896aaeaa56ac GIT binary patch literal 3318 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 A?*IS*
index 49c60505f46523e7c3b0a0e5f052c7b450f26c78..b965b73d55b725a4b69c2a5620b761e7435f5ab1 GIT binary patch literal 3967 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 Z9RQGDd}f+!o@W36002ovPDHLkV1m^HVb1^n
index 4dbb94f98f0acf7f1a3df73ff3fbf4eb9345f2bf..5de342bda53b604fe2413c8e769e7bbf14e70dc0 GIT binary patch literal 3259 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
--- a/b2g/chrome/content/shell.js +++ b/b2g/chrome/content/shell.js @@ -832,16 +832,17 @@ var WebappsHelper = { case "webapps-launch": DOMApplicationRegistry.getManifestFor(json.origin, function(aManifest) { if (!aManifest) return; let manifest = new ManifestHelper(aManifest, json.origin); shell.sendChromeEvent({ "type": "webapps-launch", + "timestamp": json.timestamp, "url": manifest.fullLaunchPath(json.startPoint), "manifestURL": json.manifestURL }); }); break; case "webapps-ask-install": let id = this.registerInstaller(json); shell.sendChromeEvent({
--- a/b2g/chrome/content/touchcontrols.css +++ b/b2g/chrome/content/touchcontrols.css @@ -22,46 +22,59 @@ width: 100%; } .controlsSpacer { display: none; -moz-box-flex: 0; } +.fullscreenButton, .playButton, .muteButton { -moz-appearance: none; min-height: 42px; min-width: 42px; border: none !important; } +.fullscreenButton { + background: url("chrome://browser/content/images/fullscreen-hdpi.png") no-repeat center; +} + +.fullscreenButton[fullscreened="true"] { + background: url("chrome://browser/content/images/exitfullscreen-hdpi.png") no-repeat center; +} + .playButton { - -moz-transform: translateX(21px); background: url("chrome://browser/content/images/pause-hdpi.png") no-repeat center; } +/* + * Normally the button bar has fullscreen spacer play spacer mute, but if + * this is an audio control rather than a video control, the fullscreen button + * is hidden by videocontrols.xml, and that alters the position of the + * play button. This workaround moves it back to center. + */ +.controlBar.audio .playButton { + transform: translateX(28px); +} + .playButton[paused="true"] { background: url("chrome://browser/content/images/play-hdpi.png") no-repeat center; } .muteButton { background: url("chrome://browser/content/images/mute-hdpi.png") no-repeat center; } .muteButton[muted="true"] { background: url("chrome://browser/content/images/unmute-hdpi.png") no-repeat center; } -/* This button is hidden until bug 704229 is fixed. */ -.fullscreenButton { - display: none; -} - /* bars */ .scrubberStack { width: 100%; min-height: 32px; max-height: 32px; padding: 0px 8px; margin: 0px; } @@ -73,16 +86,17 @@ .scrubber, .scrubber .scale-slider, .scrubber .scale-thumb { -moz-appearance: none; border: none; padding: 0px; margin: 0px; background-color: transparent; + border-radius: 3px; } .bufferBar { border: 1px solid #5e6166; } .bufferBar, .progressBar { @@ -163,27 +177,24 @@ -moz-transition-delay: 750ms; } .statusOverlay[fadeout] { opacity: 0; } .volumeStack, +.controlBar[firstshow="true"] .fullscreenButton, .controlBar[firstshow="true"] .muteButton, .controlBar[firstshow="true"] .scrubberStack, .controlBar[firstshow="true"] .durationBox, .timeLabel { display: none; } -.controlBar[firstshow="true"] .playButton { - -moz-transform: none; -} - /* Error description formatting */ .errorLabel { font-family: Helvetica, Arial, sans-serif; font-size: 11px; color: #bbb; text-shadow: -1px -1px 0 #000, 1px -1px 0 #000,
--- a/b2g/chrome/jar.mn +++ b/b2g/chrome/jar.mn @@ -36,10 +36,12 @@ chrome.jar: content/images/errorpage-larry-black.png (content/images/errorpage-larry-black.png) content/images/errorpage-larry-white.png (content/images/errorpage-larry-white.png) content/images/errorpage-warning.png (content/images/errorpage-warning.png) content/images/scrubber-hdpi.png (content/images/scrubber-hdpi.png) content/images/unmute-hdpi.png (content/images/unmute-hdpi.png) content/images/pause-hdpi.png (content/images/pause-hdpi.png) content/images/play-hdpi.png (content/images/play-hdpi.png) content/images/mute-hdpi.png (content/images/mute-hdpi.png) + content/images/fullscreen-hdpi.png (content/images/fullscreen-hdpi.png) + content/images/exitfullscreen-hdpi.png (content/images/exitfullscreen-hdpi.png) content/images/throbber.png (content/images/throbber.png) content/images/error.png (content/images/error.png)
--- a/b2g/config/panda/releng-pandaboard.tt +++ b/b2g/config/panda/releng-pandaboard.tt @@ -1,8 +1,8 @@ [ { -"size": 676768064, -"digest": "d51092cc586934f21b84e62447f49d32548cd3f222a092f2b7b8c04ff59231c97969140e0b1d897c22bcb12b8bf6a9c8b24e55891896c7eeb9bc0f190980813d", +"size": 678628568, +"digest": "e0b98a75831313171781d91ab4c3b8ae66e3af7fcec433a33d75fbc647cb3bba311f790ccb9cd9c8be8c5549210b759d6b85afe7395ada74c2c319a29556fd8e", "algorithm": "sha512", "filename": "gonk.tar.xz" } ]
--- a/b2g/test/emulator.manifest +++ b/b2g/test/emulator.manifest @@ -1,6 +1,6 @@ [{ -"size": 631436376, -"digest": "cea07d65a39a244961f183711b14d054c90690b69a79d89a3783f9a634f9ace7f6e70033e963a4f58ca8482b3aec8d4c5d3227cc7a0bc61e6afeccf2acc1a789", +"size": 608318343, +"digest": "9ab6487eccf44b0781cc96c2af9ba497f720a8d289bde40e29417f9db82788d6c8653c7dafa7443069f5898635eef45fa048ee99c03a9d0113c019a2f80f5aa8", "algorithm": "sha512", "filename": "emulator.zip" -}] \ No newline at end of file +}]
--- a/browser/base/content/test/browser_sanitize-timespans.js +++ b/browser/base/content/test/browser_sanitize-timespans.js @@ -1,28 +1,29 @@ // Bug 453440 - Test the timespan-based logic of the sanitizer code var now_uSec = Date.now() * 1000; const dm = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager); -const bhist = Cc["@mozilla.org/browser/global-history;2"].getService(Ci.nsIBrowserHistory); const formhist = Cc["@mozilla.org/satchel/form-history;1"].getService(Ci.nsIFormHistory2); const kUsecPerMin = 60 * 1000000; let tempScope = {}; Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader) .loadSubScript("chrome://browser/content/sanitize.js", tempScope); let Sanitizer = tempScope.Sanitizer; function test() { waitForExplicitFinish(); setupDownloads(); setupFormHistory(); - setupHistory(onHistoryReady); + setupHistory(function() { + Task.spawn(onHistoryReady).then(finish); + }); } function onHistoryReady() { var hoursSinceMidnight = new Date().getHours(); var minutesSinceMidnight = hoursSinceMidnight * 60 + new Date().getMinutes(); // Should test cookies here, but nsICookieManager/nsICookieService // doesn't let us fake creation times. bug 463127 @@ -40,29 +41,38 @@ function onHistoryReady() { itemPrefs.setBoolPref("passwords", false); itemPrefs.setBoolPref("sessions", false); itemPrefs.setBoolPref("siteSettings", false); // Clear 10 minutes ago s.range = [now_uSec - 10*60*1000000, now_uSec]; s.sanitize(); s.range = null; - - ok(!bhist.isVisited(makeURI("http://10minutes.com")), "10minutes.com should now be deleted"); - ok(bhist.isVisited(makeURI("http://1hour.com")), "Pretend visit to 1hour.com should still exist"); - ok(bhist.isVisited(makeURI("http://1hour10minutes.com/")), "Pretend visit to 1hour10minutes.com should still exist"); - ok(bhist.isVisited(makeURI("http://2hour.com")), "Pretend visit to 2hour.com should still exist"); - ok(bhist.isVisited(makeURI("http://2hour10minutes.com/")), "Pretend visit to 2hour10minutes.com should still exist"); - ok(bhist.isVisited(makeURI("http://4hour.com")), "Pretend visit to 4hour.com should still exist"); - ok(bhist.isVisited(makeURI("http://4hour10minutes.com/")), "Pretend visit to 4hour10minutes.com should still exist"); - - if (minutesSinceMidnight > 10) - ok(bhist.isVisited(makeURI("http://today.com")), "Pretend visit to today.com should still exist"); - ok(bhist.isVisited(makeURI("http://before-today.com")), "Pretend visit to before-today.com should still exist"); - + + ok(!(yield promiseIsURIVisited(makeURI("http://10minutes.com"))), + "Pretend visit to 10minutes.com should now be deleted"); + ok((yield promiseIsURIVisited(makeURI("http://1hour.com"))), + "Pretend visit to 1hour.com should should still exist"); + ok((yield promiseIsURIVisited(makeURI("http://1hour10minutes.com"))), + "Pretend visit to 1hour10minutes.com should should still exist"); + ok((yield promiseIsURIVisited(makeURI("http://2hour.com"))), + "Pretend visit to 2hour.com should should still exist"); + ok((yield promiseIsURIVisited(makeURI("http://2hour10minutes.com"))), + "Pretend visit to 2hour10minutes.com should should still exist"); + ok((yield promiseIsURIVisited(makeURI("http://4hour.com"))), + "Pretend visit to 4hour.com should should still exist"); + ok((yield promiseIsURIVisited(makeURI("http://4hour10minutes.com"))), + "Pretend visit to 4hour10minutes.com should should still exist"); + if (minutesSinceMidnight > 10) { + ok((yield promiseIsURIVisited(makeURI("http://today.com"))), + "Pretend visit to today.com should still exist"); + } + ok((yield promiseIsURIVisited(makeURI("http://before-today.com"))), + "Pretend visit to before-today.com should still exist"); + ok(!formhist.nameExists("10minutes"), "10minutes form entry should be deleted"); ok(formhist.nameExists("1hour"), "1hour form entry should still exist"); ok(formhist.nameExists("1hour10minutes"), "1hour10minutes form entry should still exist"); ok(formhist.nameExists("2hour"), "2hour form entry should still exist"); ok(formhist.nameExists("2hour10minutes"), "2hour10minutes form entry should still exist"); ok(formhist.nameExists("4hour"), "4hour form entry should still exist"); ok(formhist.nameExists("4hour10minutes"), "4hour10minutes form entry should still exist"); if (minutesSinceMidnight > 10) @@ -79,28 +89,36 @@ function onHistoryReady() { ok(downloadExists(5555558), "4 hour 10 minute download should still be present"); if (minutesSinceMidnight > 10) ok(downloadExists(5555554), "'Today' download should still be present"); // Clear 1 hour Sanitizer.prefs.setIntPref("timeSpan", 1); s.sanitize(); - - ok(!bhist.isVisited(makeURI("http://1hour.com")), "1hour.com should now be deleted"); - ok(bhist.isVisited(makeURI("http://1hour10minutes.com/")), "Pretend visit to 1hour10minutes.com should still exist"); - ok(bhist.isVisited(makeURI("http://2hour.com")), "Pretend visit to 2hour.com should still exist"); - ok(bhist.isVisited(makeURI("http://2hour10minutes.com/")), "Pretend visit to 2hour10minutes.com should still exist"); - ok(bhist.isVisited(makeURI("http://4hour.com")), "Pretend visit to 4hour.com should still exist"); - ok(bhist.isVisited(makeURI("http://4hour10minutes.com/")), "Pretend visit to 4hour10minutes.com should still exist"); - - if (hoursSinceMidnight > 1) - ok(bhist.isVisited(makeURI("http://today.com")), "Pretend visit to today.com should still exist"); - ok(bhist.isVisited(makeURI("http://before-today.com")), "Pretend visit to before-today.com should still exist"); - + + ok(!(yield promiseIsURIVisited(makeURI("http://1hour.com"))), + "Pretend visit to 1hour.com should now be deleted"); + ok((yield promiseIsURIVisited(makeURI("http://1hour10minutes.com"))), + "Pretend visit to 1hour10minutes.com should should still exist"); + ok((yield promiseIsURIVisited(makeURI("http://2hour.com"))), + "Pretend visit to 2hour.com should should still exist"); + ok((yield promiseIsURIVisited(makeURI("http://2hour10minutes.com"))), + "Pretend visit to 2hour10minutes.com should should still exist"); + ok((yield promiseIsURIVisited(makeURI("http://4hour.com"))), + "Pretend visit to 4hour.com should should still exist"); + ok((yield promiseIsURIVisited(makeURI("http://4hour10minutes.com"))), + "Pretend visit to 4hour10minutes.com should should still exist"); + if (hoursSinceMidnight > 1) { + ok((yield promiseIsURIVisited(makeURI("http://today.com"))), + "Pretend visit to today.com should still exist"); + } + ok((yield promiseIsURIVisited(makeURI("http://before-today.com"))), + "Pretend visit to before-today.com should still exist"); + ok(!formhist.nameExists("1hour"), "1hour form entry should be deleted"); ok(formhist.nameExists("1hour10minutes"), "1hour10minutes form entry should still exist"); ok(formhist.nameExists("2hour"), "2hour form entry should still exist"); ok(formhist.nameExists("2hour10minutes"), "2hour10minutes form entry should still exist"); ok(formhist.nameExists("4hour"), "4hour form entry should still exist"); ok(formhist.nameExists("4hour10minutes"), "4hour10minutes form entry should still exist"); if (hoursSinceMidnight > 1) ok(formhist.nameExists("today"), "today form entry should still exist"); @@ -116,26 +134,34 @@ function onHistoryReady() { if (hoursSinceMidnight > 1) ok(downloadExists(5555554), "'Today' download should still be present"); // Clear 1 hour 10 minutes s.range = [now_uSec - 70*60*1000000, now_uSec]; s.sanitize(); s.range = null; - - ok(!bhist.isVisited(makeURI("http://1hour10minutes.com")), "Pretend visit to 1hour10minutes.com should now be deleted"); - ok(bhist.isVisited(makeURI("http://2hour.com")), "Pretend visit to 2hour.com should still exist"); - ok(bhist.isVisited(makeURI("http://2hour10minutes.com/")), "Pretend visit to 2hour10minutes.com should still exist"); - ok(bhist.isVisited(makeURI("http://4hour.com")), "Pretend visit to 4hour.com should still exist"); - ok(bhist.isVisited(makeURI("http://4hour10minutes.com/")), "Pretend visit to 4hour10minutes.com should still exist"); - if (minutesSinceMidnight > 70) - ok(bhist.isVisited(makeURI("http://today.com")), "Pretend visit to today.com should still exist"); - ok(bhist.isVisited(makeURI("http://before-today.com")), "Pretend visit to before-today.com should still exist"); - + + ok(!(yield promiseIsURIVisited(makeURI("http://1hour10minutes.com"))), + "Pretend visit to 1hour10minutes.com should now be deleted"); + ok((yield promiseIsURIVisited(makeURI("http://2hour.com"))), + "Pretend visit to 2hour.com should should still exist"); + ok((yield promiseIsURIVisited(makeURI("http://2hour10minutes.com"))), + "Pretend visit to 2hour10minutes.com should should still exist"); + ok((yield promiseIsURIVisited(makeURI("http://4hour.com"))), + "Pretend visit to 4hour.com should should still exist"); + ok((yield promiseIsURIVisited(makeURI("http://4hour10minutes.com"))), + "Pretend visit to 4hour10minutes.com should should still exist"); + if (minutesSinceMidnight > 70) { + ok((yield promiseIsURIVisited(makeURI("http://today.com"))), + "Pretend visit to today.com should still exist"); + } + ok((yield promiseIsURIVisited(makeURI("http://before-today.com"))), + "Pretend visit to before-today.com should still exist"); + ok(!formhist.nameExists("1hour10minutes"), "1hour10minutes form entry should be deleted"); ok(formhist.nameExists("2hour"), "2hour form entry should still exist"); ok(formhist.nameExists("2hour10minutes"), "2hour10minutes form entry should still exist"); ok(formhist.nameExists("4hour"), "4hour form entry should still exist"); ok(formhist.nameExists("4hour10minutes"), "4hour10minutes form entry should still exist"); if (minutesSinceMidnight > 70) ok(formhist.nameExists("today"), "today form entry should still exist"); ok(formhist.nameExists("b4today"), "b4today form entry should still exist"); @@ -147,25 +173,32 @@ function onHistoryReady() { ok(downloadExists(5555553), "<4 hour old download should still be present"); ok(downloadExists(5555558), "4 hour 10 minute download should still be present"); if (minutesSinceMidnight > 70) ok(downloadExists(5555554), "'Today' download should still be present"); // Clear 2 hours Sanitizer.prefs.setIntPref("timeSpan", 2); s.sanitize(); - - ok(!bhist.isVisited(makeURI("http://2hour.com")), "Pretend visit to 2hour.com should now be deleted"); - ok(bhist.isVisited(makeURI("http://2hour10minutes.com/")), "Pretend visit to 2hour10minutes.com should still exist"); - ok(bhist.isVisited(makeURI("http://4hour.com")), "Pretend visit to 4hour.com should still exist"); - ok(bhist.isVisited(makeURI("http://4hour10minutes.com/")), "Pretend visit to 4hour10minutes.com should still exist"); - if (hoursSinceMidnight > 2) - ok(bhist.isVisited(makeURI("http://today.com")), "Pretend visit to today.com should still exist"); - ok(bhist.isVisited(makeURI("http://before-today.com")), "Pretend visit to before-today.com should still exist"); - + + ok(!(yield promiseIsURIVisited(makeURI("http://2hour.com"))), + "Pretend visit to 2hour.com should now be deleted"); + ok((yield promiseIsURIVisited(makeURI("http://2hour10minutes.com"))), + "Pretend visit to 2hour10minutes.com should should still exist"); + ok((yield promiseIsURIVisited(makeURI("http://4hour.com"))), + "Pretend visit to 4hour.com should should still exist"); + ok((yield promiseIsURIVisited(makeURI("http://4hour10minutes.com"))), + "Pretend visit to 4hour10minutes.com should should still exist"); + if (hoursSinceMidnight > 2) { + ok((yield promiseIsURIVisited(makeURI("http://today.com"))), + "Pretend visit to today.com should still exist"); + } + ok((yield promiseIsURIVisited(makeURI("http://before-today.com"))), + "Pretend visit to before-today.com should still exist"); + ok(!formhist.nameExists("2hour"), "2hour form entry should be deleted"); ok(formhist.nameExists("2hour10minutes"), "2hour10minutes form entry should still exist"); ok(formhist.nameExists("4hour"), "4hour form entry should still exist"); ok(formhist.nameExists("4hour10minutes"), "4hour10minutes form entry should still exist"); if (hoursSinceMidnight > 2) ok(formhist.nameExists("today"), "today form entry should still exist"); ok(formhist.nameExists("b4today"), "b4today form entry should still exist"); @@ -177,24 +210,30 @@ function onHistoryReady() { ok(downloadExists(5555558), "4 hour 10 minute download should still be present"); if (hoursSinceMidnight > 2) ok(downloadExists(5555554), "'Today' download should still be present"); // Clear 2 hours 10 minutes s.range = [now_uSec - 130*60*1000000, now_uSec]; s.sanitize(); s.range = null; - - ok(!bhist.isVisited(makeURI("http://2hour10minutes.com")), "Pretend visit to 2hour10minutes.com should now be deleted"); - ok(bhist.isVisited(makeURI("http://4hour.com")), "Pretend visit to 4hour.com should still exist"); - ok(bhist.isVisited(makeURI("http://4hour10minutes.com/")), "Pretend visit to 4hour10minutes.com should still exist"); - if (minutesSinceMidnight > 130) - ok(bhist.isVisited(makeURI("http://today.com")), "Pretend visit to today.com should still exist"); - ok(bhist.isVisited(makeURI("http://before-today.com")), "Pretend visit to before-today.com should still exist"); - + + ok(!(yield promiseIsURIVisited(makeURI("http://2hour10minutes.com"))), + "Pretend visit to 2hour10minutes.com should now be deleted"); + ok((yield promiseIsURIVisited(makeURI("http://4hour.com"))), + "Pretend visit to 4hour.com should should still exist"); + ok((yield promiseIsURIVisited(makeURI("http://4hour10minutes.com"))), + "Pretend visit to 4hour10minutes.com should should still exist"); + if (minutesSinceMidnight > 130) { + ok((yield promiseIsURIVisited(makeURI("http://today.com"))), + "Pretend visit to today.com should still exist"); + } + ok((yield promiseIsURIVisited(makeURI("http://before-today.com"))), + "Pretend visit to before-today.com should still exist"); + ok(!formhist.nameExists("2hour10minutes"), "2hour10minutes form entry should be deleted"); ok(formhist.nameExists("4hour"), "4hour form entry should still exist"); ok(formhist.nameExists("4hour10minutes"), "4hour10minutes form entry should still exist"); if (minutesSinceMidnight > 130) ok(formhist.nameExists("today"), "today form entry should still exist"); ok(formhist.nameExists("b4today"), "b4today form entry should still exist"); ok(!downloadExists(5555557), "2 hour 10 minute old download should now be deleted"); @@ -202,44 +241,53 @@ function onHistoryReady() { ok(downloadExists(5555558), "4 hour 10 minute download should still be present"); ok(downloadExists(5555550), "Year old download should still be present"); if (minutesSinceMidnight > 130) ok(downloadExists(5555554), "'Today' download should still be present"); // Clear 4 hours Sanitizer.prefs.setIntPref("timeSpan", 3); s.sanitize(); - - ok(!bhist.isVisited(makeURI("http://4hour.com")), "Pretend visit to 4hour.com should now be deleted"); - ok(bhist.isVisited(makeURI("http://4hour10minutes.com/")), "Pretend visit to 4hour10minutes.com should still exist"); - if (hoursSinceMidnight > 4) - ok(bhist.isVisited(makeURI("http://today.com")), "Pretend visit to today.com should still exist"); - ok(bhist.isVisited(makeURI("http://before-today.com")), "Pretend visit to before-today.com should still exist"); - + + ok(!(yield promiseIsURIVisited(makeURI("http://4hour.com"))), + "Pretend visit to 4hour.com should now be deleted"); + ok((yield promiseIsURIVisited(makeURI("http://4hour10minutes.com"))), + "Pretend visit to 4hour10minutes.com should should still exist"); + if (hoursSinceMidnight > 4) { + ok((yield promiseIsURIVisited(makeURI("http://today.com"))), + "Pretend visit to today.com should still exist"); + } + ok((yield promiseIsURIVisited(makeURI("http://before-today.com"))), + "Pretend visit to before-today.com should still exist"); + ok(!formhist.nameExists("4hour"), "4hour form entry should be deleted"); ok(formhist.nameExists("4hour10minutes"), "4hour10minutes form entry should still exist"); if (hoursSinceMidnight > 4) ok(formhist.nameExists("today"), "today form entry should still exist"); ok(formhist.nameExists("b4today"), "b4today form entry should still exist"); ok(!downloadExists(5555553), "<4 hour old download should now be deleted"); ok(downloadExists(5555558), "4 hour 10 minute download should still be present"); ok(downloadExists(5555550), "Year old download should still be present"); if (hoursSinceMidnight > 4) ok(downloadExists(5555554), "'Today' download should still be present"); // Clear 4 hours 10 minutes s.range = [now_uSec - 250*60*1000000, now_uSec]; s.sanitize(); s.range = null; - - ok(!bhist.isVisited(makeURI("http://4hour10minutes.com/")), "Pretend visit to 4hour10minutes.com should now be deleted"); - if (minutesSinceMidnight > 250) - ok(bhist.isVisited(makeURI("http://today.com")), "Pretend visit to today.com should still exist"); - ok(bhist.isVisited(makeURI("http://before-today.com")), "Pretend visit to before-today.com should still exist"); + + ok(!(yield promiseIsURIVisited(makeURI("http://4hour10minutes.com"))), + "Pretend visit to 4hour10minutes.com should now be deleted"); + if (minutesSinceMidnight > 250) { + ok((yield promiseIsURIVisited(makeURI("http://today.com"))), + "Pretend visit to today.com should still exist"); + } + ok((yield promiseIsURIVisited(makeURI("http://before-today.com"))), + "Pretend visit to before-today.com should still exist"); ok(!formhist.nameExists("4hour10minutes"), "4hour10minutes form entry should be deleted"); if (minutesSinceMidnight > 250) ok(formhist.nameExists("today"), "today form entry should still exist"); ok(formhist.nameExists("b4today"), "b4today form entry should still exist"); ok(!downloadExists(5555558), "4 hour 10 minute download should now be deleted"); ok(downloadExists(5555550), "Year old download should still be present"); @@ -252,36 +300,37 @@ function onHistoryReady() { // Be careful. If we add our objectss just before midnight, and sanitize // runs immediately after, they won't be expired. This is expected, but // we should not test in that case. We cannot just test for opposite // condition because we could cross midnight just one moment after we // cache our time, then we would have an even worse random failure. var today = isToday(new Date(now_uSec/1000)); if (today) { - ok(!bhist.isVisited(makeURI("http://today.com")), "Pretend visit to today.com should now be deleted"); + ok(!(yield promiseIsURIVisited(makeURI("http://today.com"))), + "Pretend visit to today.com should now be deleted"); ok(!formhist.nameExists("today"), "today form entry should be deleted"); ok(!downloadExists(5555554), "'Today' download should now be deleted"); } - ok(bhist.isVisited(makeURI("http://before-today.com")), "Pretend visit to before-today.com should still exist"); + ok((yield promiseIsURIVisited(makeURI("http://before-today.com"))), + "Pretend visit to before-today.com should still exist"); ok(formhist.nameExists("b4today"), "b4today form entry should still exist"); ok(downloadExists(5555550), "Year old download should still be present"); // Choose everything Sanitizer.prefs.setIntPref("timeSpan", 0); s.sanitize(); - ok(!bhist.isVisited(makeURI("http://before-today.com")), "Pretend visit to before-today.com should now be deleted"); + ok(!(yield promiseIsURIVisited(makeURI("http://before-today.com"))), + "Pretend visit to before-today.com should now be deleted"); ok(!formhist.nameExists("b4today"), "b4today form entry should be deleted"); ok(!downloadExists(5555550), "Year old download should now be deleted"); - - finish(); } function setupHistory(aCallback) { let places = []; function addPlace(aURI, aTitle, aVisitDate) { places.push({ uri: aURI,
--- a/browser/base/content/test/browser_sanitizeDialog.js +++ b/browser/base/content/test/browser_sanitizeDialog.js @@ -72,20 +72,21 @@ var gAllTests = [ // Show details this.toggleDetails(); this.checkDetails(true); // Hide details this.toggleDetails(); this.checkDetails(false); this.cancelDialog(); - - ensureHistoryClearedState(uris, false); + }; + wh.onunload = function () { + yield promiseHistoryClearedState(uris, false); blankSlate(); - ensureHistoryClearedState(uris, true); + yield promiseHistoryClearedState(uris, true); }; wh.open(); }); }, /** * Ensures that the combined history-downloads checkbox clears both history * visits and downloads when checked; the dialog respects simple timespan. @@ -131,28 +132,29 @@ var gAllTests = [ "timeSpan pref should be hour after accepting dialog with " + "hour selected"); boolPrefIs("cpd.history", true, "history pref should be true after accepting dialog with " + "history checkbox checked"); boolPrefIs("cpd.downloads", true, "downloads pref should be true after accepting dialog with " + "history checkbox checked"); - + }; + wh.onunload = function () { // History visits and downloads within one hour should be cleared. - ensureHistoryClearedState(uris, true); + yield promiseHistoryClearedState(uris, true); ensureDownloadsClearedState(downloadIDs, true); // Visits and downloads > 1 hour should still exist. - ensureHistoryClearedState(olderURIs, false); + yield promiseHistoryClearedState(olderURIs, false); ensureDownloadsClearedState(olderDownloadIDs, false); // OK, done, cleanup after ourselves. blankSlate(); - ensureHistoryClearedState(olderURIs, true); + yield promiseHistoryClearedState(olderURIs, true); ensureDownloadsClearedState(olderDownloadIDs, true); }; wh.open(); }); }, /** * Ensures that the combined history-downloads checkbox removes neither @@ -195,25 +197,26 @@ var gAllTests = [ "timeSpan pref should be hour after accepting dialog with " + "hour selected"); boolPrefIs("cpd.history", false, "history pref should be false after accepting dialog with " + "history checkbox unchecked"); boolPrefIs("cpd.downloads", false, "downloads pref should be false after accepting dialog with " + "history checkbox unchecked"); - + }; + wh.onunload = function () { // Of the three only form entries should be cleared. - ensureHistoryClearedState(uris, false); + yield promiseHistoryClearedState(uris, false); ensureDownloadsClearedState(downloadIDs, false); ensureFormEntriesClearedState(formEntries, true); // OK, done, cleanup after ourselves. blankSlate(); - ensureHistoryClearedState(uris, true); + yield promiseHistoryClearedState(uris, true); ensureDownloadsClearedState(downloadIDs, true); }; wh.open(); }); }, /** * Ensures that the "Everything" duration option works. @@ -248,17 +251,19 @@ var gAllTests = [ this.toggleDetails(); this.checkDetails(true); this.acceptDialog(); intPrefIs("sanitize.timeSpan", Sanitizer.TIMESPAN_EVERYTHING, "timeSpan pref should be everything after accepting dialog " + "with everything selected"); - ensureHistoryClearedState(uris, true); + }; + wh.onunload = function () { + yield promiseHistoryClearedState(uris, true); }; wh.open(); }); }, /** * Ensures that the "Everything" warning is visible on dialog open after * the previous test. @@ -283,17 +288,19 @@ var gAllTests = [ "with clearing everything"); this.selectDuration(Sanitizer.TIMESPAN_EVERYTHING); this.checkPrefCheckbox("history", true); this.acceptDialog(); intPrefIs("sanitize.timeSpan", Sanitizer.TIMESPAN_EVERYTHING, "timeSpan pref should be everything after accepting dialog " + "with everything selected"); - ensureHistoryClearedState(uris, true); + }; + wh.onunload = function () { + yield promiseHistoryClearedState(uris, true); }; wh.open(); }); }, /** * The next three tests checks that when a certain history item cannot be * cleared then the checkbox should be both disabled and unchecked. @@ -316,18 +323,19 @@ var gAllTests = [ var cb = this.win.document.querySelectorAll( "#itemList > [preference='privacy.cpd.history']"); ok(cb.length == 1 && !cb[0].disabled, "There is history, checkbox to " + "clear history should be enabled."); this.checkAllCheckboxes(); this.acceptDialog(); - - ensureHistoryClearedState(uris, true); + }; + wh.onunload = function () { + yield promiseHistoryClearedState(uris, true); ensureFormEntriesClearedState(formEntries, true); }; wh.open(); }); }, function () { let wh = new WindowHelper(); wh.onload = function() { @@ -368,16 +376,18 @@ var gAllTests = [ var cb = this.win.document.querySelectorAll( "#itemList > [preference='privacy.cpd.formdata']"); ok(cb.length == 1 && !cb[0].disabled && cb[0].checked, "There exists formEntries so the checkbox should be in sync with " + "the pref."); this.acceptDialog(); + }; + wh.onunload = function () { ensureFormEntriesClearedState(formEntries, true); }; wh.open(); }, /** * These next six tests together ensure that toggling details persists @@ -778,19 +788,23 @@ WindowHelper.prototype = { win.removeEventListener("unload", onunload, false); wh.win = win; executeSoon(function () { // Some exceptions that reach here don't reach the test harness, but // ok()/is() do... try { - if (wh.onunload) - wh.onunload(); - waitForAsyncUpdates(doNextTest); + if (wh.onunload) { + Task.spawn(wh.onunload).then(function() { + waitForAsyncUpdates(doNextTest); + }); + } else { + waitForAsyncUpdates(doNextTest); + } } catch (exc) { win.close(); ok(false, "Unexpected exception: " + exc + "\n" + exc.stack); finish(); } }); }, false); @@ -897,55 +911,16 @@ function addFormEntryWithMinutesAgo(aMin */ function blankSlate() { PlacesUtils.bhistory.removeAllPages(); dm.cleanUp(); formhist.removeAllEntries(); } /** - * Waits for all pending async statements on the default connection, before - * proceeding with aCallback. - * - * @param aCallback - * Function to be called when done. - * @param aScope - * Scope for the callback. - * @param aArguments - * Arguments array for the callback. - * - * @note The result is achieved by asynchronously executing a query requiring - * a write lock. Since all statements on the same connection are - * serialized, the end of this write operation means that all writes are - * complete. Note that WAL makes so that writers don't block readers, but - * this is a problem only across different connections. - */ -function waitForAsyncUpdates(aCallback, aScope, aArguments) -{ - let scope = aScope || this; - let args = aArguments || []; - let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) - .DBConnection; - let begin = db.createAsyncStatement("BEGIN EXCLUSIVE"); - begin.executeAsync(); - begin.finalize(); - - let commit = db.createAsyncStatement("COMMIT"); - commit.executeAsync({ - handleResult: function() {}, - handleError: function() {}, - handleCompletion: function(aReason) - { - aCallback.apply(scope, args); - } - }); - commit.finalize(); -} - -/** * Ensures that the given pref is the expected value. * * @param aPrefName * The pref's sub-branch under the privacy branch * @param aExpectedVal * The pref's expected value * @param aMsg * Passed to is() @@ -1019,32 +994,16 @@ function ensureFormEntriesClearedState(a let niceStr = aShouldBeCleared ? "no longer" : "still"; aFormEntries.forEach(function (entry) { is(formhist.nameExists(entry), !aShouldBeCleared, "form entry " + entry + " should " + niceStr + " exist"); }); } /** - * Ensures that the specified URIs are either cleared or not. - * - * @param aURIs - * Array of page URIs - * @param aShouldBeCleared - * True if each visit to the URI should be cleared, false otherwise - */ -function ensureHistoryClearedState(aURIs, aShouldBeCleared) { - let niceStr = aShouldBeCleared ? "no longer" : "still"; - aURIs.forEach(function (aURI) { - is(PlacesUtils.bhistory.isVisited(aURI), !aShouldBeCleared, - "history visit " + aURI.spec + " should " + niceStr + " exist"); - }); -} - -/** * Ensures that the given pref is the expected value. * * @param aPrefName * The pref's sub-branch under the privacy branch * @param aExpectedVal * The pref's expected value * @param aMsg * Passed to is()
--- a/browser/base/content/test/browser_sanitizeDialog_treeView.js +++ b/browser/base/content/test/browser_sanitizeDialog_treeView.js @@ -72,21 +72,21 @@ var gAllTests = [ wh.checkGrippy("Grippy should remain at last row after trying to move " + "it down", wh.getRowCount() - 1); // Cancel the dialog, make sure history visits are not cleared. wh.checkPrefCheckbox("history", false); wh.cancelDialog(); - ensureHistoryClearedState(uris, false); + yield promiseHistoryClearedState(uris, false); // OK, done, cleanup after ourselves. blankSlate(); - ensureHistoryClearedState(uris, true); + yield promiseHistoryClearedState(uris, true); }); }); }, /** * Ensures that the combined history-downloads checkbox clears both history * visits and downloads when checked; the dialog respects simple timespan. */ @@ -128,26 +128,26 @@ var gAllTests = [ wh.checkGrippy("Grippy should be at proper row after selecting HOUR " + "duration", uris.length); // Accept the dialog, make sure history visits and downloads within one // hour are cleared. wh.checkPrefCheckbox("history", true); wh.acceptDialog(); - ensureHistoryClearedState(uris, true); + yield promiseHistoryClearedState(uris, true); ensureDownloadsClearedState(downloadIDs, true); // Make sure visits and downloads > 1 hour still exist. - ensureHistoryClearedState(olderURIs, false); + yield promiseHistoryClearedState(olderURIs, false); ensureDownloadsClearedState(olderDownloadIDs, false); // OK, done, cleanup after ourselves. blankSlate(); - ensureHistoryClearedState(olderURIs, true); + yield promiseHistoryClearedState(olderURIs, true); ensureDownloadsClearedState(olderDownloadIDs, true); }); }); }, /** * Ensures that the combined history-downloads checkbox removes neither * history visits nor downloads when not checked. @@ -182,23 +182,23 @@ var gAllTests = [ wh.getRowCount() - 1); // Remove only form entries, leave history (including downloads). wh.checkPrefCheckbox("history", false); wh.checkPrefCheckbox("formdata", true); wh.acceptDialog(); // Of the three only form entries should be cleared. - ensureHistoryClearedState(uris, false); + yield promiseHistoryClearedState(uris, false); ensureDownloadsClearedState(downloadIDs, false); ensureFormEntriesClearedState(formEntries, true); // OK, done, cleanup after ourselves. blankSlate(); - ensureHistoryClearedState(uris, true); + yield promiseHistoryClearedState(uris, true); ensureDownloadsClearedState(downloadIDs, true); }); }); }, /** * Ensures that the "Everything" duration option works. */ @@ -217,17 +217,17 @@ var gAllTests = [ addVisits(places, function() { // Open the dialog and do our tests. openWindow(function (aWin) { let wh = new WindowHelper(aWin); wh.selectDuration(Sanitizer.TIMESPAN_EVERYTHING); wh.checkPrefCheckbox("history", true); wh.acceptDialog(); - ensureHistoryClearedState(uris, true); + yield promiseHistoryClearedState(uris, true); }); }); } ]; // Used as the download database ID for a new download. Incremented for each // new download. See addDownloadWithMinutesAgo(). var gDownloadId = 5555551; @@ -498,55 +498,16 @@ function addFormEntryWithMinutesAgo(aMin */ function blankSlate() { PlacesUtils.bhistory.removeAllPages(); dm.cleanUp(); formhist.removeAllEntries(); } /** - * Waits for all pending async statements on the default connection, before - * proceeding with aCallback. - * - * @param aCallback - * Function to be called when done. - * @param aScope - * Scope for the callback. - * @param aArguments - * Arguments array for the callback. - * - * @note The result is achieved by asynchronously executing a query requiring - * a write lock. Since all statements on the same connection are - * serialized, the end of this write operation means that all writes are - * complete. Note that WAL makes so that writers don't block readers, but - * this is a problem only across different connections. - */ -function waitForAsyncUpdates(aCallback, aScope, aArguments) -{ - let scope = aScope || this; - let args = aArguments || []; - let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) - .DBConnection; - let begin = db.createAsyncStatement("BEGIN EXCLUSIVE"); - begin.executeAsync(); - begin.finalize(); - - let commit = db.createAsyncStatement("COMMIT"); - commit.executeAsync({ - handleResult: function() {}, - handleError: function() {}, - handleCompletion: function(aReason) - { - aCallback.apply(scope, args); - } - }); - commit.finalize(); -} - -/** * Checks to see if the download with the specified ID exists. * * @param aID * The ID of the download to check * @return True if the download exists, false otherwise */ function downloadExists(aID) { @@ -606,32 +567,16 @@ function ensureFormEntriesClearedState(a let niceStr = aShouldBeCleared ? "no longer" : "still"; aFormEntries.forEach(function (entry) { is(formhist.nameExists(entry), !aShouldBeCleared, "form entry " + entry + " should " + niceStr + " exist"); }); } /** - * Ensures that the specified URIs are either cleared or not. - * - * @param aURIs - * Array of page URIs - * @param aShouldBeCleared - * True if each visit to the URI should be cleared, false otherwise - */ -function ensureHistoryClearedState(aURIs, aShouldBeCleared) { - let niceStr = aShouldBeCleared ? "no longer" : "still"; - aURIs.forEach(function (aURI) { - is(PlacesUtils.bhistory.isVisited(aURI), !aShouldBeCleared, - "history visit " + aURI.spec + " should " + niceStr + " exist"); - }); -} - -/** * Opens the sanitize dialog and runs a callback once it's finished loading. * * @param aOnloadCallback * A function that will be called once the dialog has loaded */ function openWindow(aOnloadCallback) { function windowObserver(aSubject, aTopic, aData) { if (aTopic != "domwindowopened") @@ -640,18 +585,21 @@ function openWindow(aOnloadCallback) { Services.ww.unregisterNotification(windowObserver); let win = aSubject.QueryInterface(Ci.nsIDOMWindow); win.addEventListener("load", function onload(event) { win.removeEventListener("load", onload, false); executeSoon(function () { // Some exceptions that reach here don't reach the test harness, but // ok()/is() do... try { - aOnloadCallback(win); - waitForAsyncUpdates(doNextTest); + Task.spawn(function() { + aOnloadCallback(win); + }).then(function() { + waitForAsyncUpdates(doNextTest); + }); } catch (exc) { win.close(); ok(false, "Unexpected exception: " + exc + "\n" + exc.stack); finish(); } }); }, false);
--- a/browser/base/content/test/head.js +++ b/browser/base/content/test/head.js @@ -1,8 +1,18 @@ +netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect'); +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "Promise", + "resource://gre/modules/commonjs/sdk/core/promise.js"); +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", + "resource://gre/modules/PlacesUtils.jsm"); + function whenDelayedStartupFinished(aWindow, aCallback) { Services.obs.addObserver(function observer(aSubject, aTopic) { if (aWindow == aSubject) { Services.obs.removeObserver(observer, aTopic); executeSoon(aCallback); } }, "browser-delayed-startup-finished", false); } @@ -127,16 +137,71 @@ function resetBlocklist() { function whenNewWindowLoaded(aOptions, aCallback) { let win = OpenBrowserWindow(aOptions); win.addEventListener("load", function onLoad() { win.removeEventListener("load", onLoad, false); aCallback(win); }, false); } +/** + * Waits for all pending async statements on the default connection, before + * proceeding with aCallback. + * + * @param aCallback + * Function to be called when done. + * @param aScope + * Scope for the callback. + * @param aArguments + * Arguments array for the callback. + * + * @note The result is achieved by asynchronously executing a query requiring + * a write lock. Since all statements on the same connection are + * serialized, the end of this write operation means that all writes are + * complete. Note that WAL makes so that writers don't block readers, but + * this is a problem only across different connections. + */ +function waitForAsyncUpdates(aCallback, aScope, aArguments) { + let scope = aScope || this; + let args = aArguments || []; + let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) + .DBConnection; + let begin = db.createAsyncStatement("BEGIN EXCLUSIVE"); + begin.executeAsync(); + begin.finalize(); + + let commit = db.createAsyncStatement("COMMIT"); + commit.executeAsync({ + handleResult: function() {}, + handleError: function() {}, + handleCompletion: function(aReason) { + aCallback.apply(scope, args); + } + }); + commit.finalize(); +} + +/** + * Asynchronously check a url is visited. + + * @param aURI The URI. + * @param aExpectedValue The expected value. + * @return {Promise} + * @resolves When the check has been added successfully. + * @rejects JavaScript exception. + */ +function promiseIsURIVisited(aURI, aExpectedValue) { + let deferred = Promise.defer(); + PlacesUtils.asyncHistory.isURIVisited(aURI, function(aURI, aIsVisited) { + deferred.resolve(aIsVisited); + }); + + return deferred.promise; +} + function addVisits(aPlaceInfo, aCallback) { let places = []; if (aPlaceInfo instanceof Ci.nsIURI) { places.push({ uri: aPlaceInfo }); } else if (Array.isArray(aPlaceInfo)) { places = places.concat(aPlaceInfo); } else { places.push(aPlaceInfo); @@ -165,8 +230,35 @@ function addVisits(aPlaceInfo, aCallback handleResult: function () {}, handleCompletion: function UP_handleCompletion() { if (aCallback) aCallback(); } } ); } + +/** + * Ensures that the specified URIs are either cleared or not. + * + * @param aURIs + * Array of page URIs + * @param aShouldBeCleared + * True if each visit to the URI should be cleared, false otherwise + */ +function promiseHistoryClearedState(aURIs, aShouldBeCleared) { + let deferred = Promise.defer(); + let callbackCount = 0; + let niceStr = aShouldBeCleared ? "no longer" : "still"; + function callbackDone() { + if (++callbackCount == aURIs.length) + deferred.resolve(); + } + aURIs.forEach(function (aURI) { + PlacesUtils.asyncHistory.isURIVisited(aURI, function(aURI, aIsVisited) { + is(aIsVisited, !aShouldBeCleared, + "history visit " + aURI.spec + " should " + niceStr + " exist"); + callbackDone(); + }); + }); + + return deferred.promise; +}
--- a/browser/base/content/test/social/browser_social_chatwindow.js +++ b/browser/base/content/test/social/browser_social_chatwindow.js @@ -186,17 +186,17 @@ var tests = { case "got-chatbox-message": ok(true, "got a chat window opened"); let chats = document.getElementById("pinnedchats"); ok(chats.selectedChat.minimized, "chatbox from worker opened as minimized"); while (chats.selectedChat) { chats.selectedChat.close(); } ok(!chats.selectedChat, "chats are all closed"); - ensureSocialUrlNotRemembered(chatUrl); + gURLsNotRemembered.push(chatUrl); port.close(); next(); break; } } port.postMessage({topic: "test-worker-chat", data: chatUrl}); }, testCloseSelf: function(next) {
--- a/browser/base/content/test/social/browser_social_mozSocial_API.js +++ b/browser/base/content/test/social/browser_social_mozSocial_API.js @@ -48,17 +48,17 @@ var tests = { switch (topic) { case "test-init-done": iconsReady = true; checkNext(); break; case "got-panel-message": ok(true, "got panel message"); // Check the panel isn't in our history. - ensureSocialUrlNotRemembered(e.data.location); + gURLsNotRemembered.push(e.data.location); break; case "got-social-panel-visibility": if (e.data.result == "shown") { ok(true, "panel shown"); let panel = document.getElementById("social-notification-panel"); panel.hidePopup(); } else if (e.data.result == "hidden") { ok(true, "panel hidden");
--- a/browser/base/content/test/social/head.js +++ b/browser/base/content/test/social/head.js @@ -1,12 +1,21 @@ /* 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/. */ +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "Promise", + "resource://gre/modules/commonjs/sdk/core/promise.js"); +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", + "resource://gre/modules/PlacesUtils.jsm"); + function waitForCondition(condition, nextTest, errorMsg) { var tries = 0; var interval = setInterval(function() { if (tries >= 30) { ok(false, errorMsg); moveOn(); } if (condition()) { @@ -14,46 +23,56 @@ function waitForCondition(condition, nex } tries++; }, 100); var moveOn = function() { clearInterval(interval); nextTest(); }; } // Check that a specified (string) URL hasn't been "remembered" (ie, is not // in history, will not appear in about:newtab or auto-complete, etc.) -function ensureSocialUrlNotRemembered(url) { - let gh = Cc["@mozilla.org/browser/global-history;2"] - .getService(Ci.nsIGlobalHistory2); +function promiseSocialUrlNotRemembered(url) { + let deferred = Promise.defer(); let uri = Services.io.newURI(url, null, null); - ok(!gh.isVisited(uri), "social URL " + url + " should not be in global history"); + PlacesUtils.asyncHistory.isURIVisited(uri, function(aURI, aIsVisited) { + ok(!aIsVisited, "social URL " + url + " should not be in global history"); + deferred.resolve(); + }); + return deferred.promise; } +let gURLsNotRemembered = []; + function runSocialTestWithProvider(manifest, callback) { let SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService; let manifests = Array.isArray(manifest) ? manifest : [manifest]; // Check that none of the provider's content ends up in history. - registerCleanupFunction(function () { - manifests.forEach(function (m) { + function finishCleanUp() { + for (let i = 0; i < manifests.length; i++) { + let m = manifests[i]; for (let what of ['sidebarURL', 'workerURL', 'iconURL']) { if (m[what]) { - ensureSocialUrlNotRemembered(m[what]); + yield promiseSocialUrlNotRemembered(m[what]); } - } - }); - }); + }; + } + for (let i = 0; i < gURLsNotRemembered.length; i++) { + yield promiseSocialUrlNotRemembered(gURLsNotRemembered[i]); + } + gURLsNotRemembered = []; + } info("runSocialTestWithProvider: " + manifests.toSource()); let finishCount = 0; function finishIfDone(callFinish) { finishCount++; if (finishCount == manifests.length) - finish(); + Task.spawn(finishCleanUp).then(finish); } function removeAddedProviders(cleanup) { manifests.forEach(function (m) { // If we're "cleaning up", don't call finish when done. let callback = cleanup ? function () {} : finishIfDone; // Similarly, if we're cleaning up, catch exceptions from removeProvider let removeProvider = SocialService.removeProvider.bind(SocialService); if (cleanup) {
--- a/browser/components/places/content/places.js +++ b/browser/components/places/content/places.js @@ -1246,20 +1246,27 @@ let ContentArea = { typeof aView != "object" && typeof aView != "function") throw new Error("Invalid arguments"); this._specialViews.set(aQueryString, { view: aView, options: aOptions || new Object() }); }, get currentView() PlacesUIUtils.getViewForNode(this._deck.selectedPanel), - set currentView(aView) { - if (this.currentView != aView) - this._deck.selectedPanel = aView.associatedElement; - return aView; + set currentView(aNewView) { + let oldView = this.currentView; + if (oldView != aNewView) { + this._deck.selectedPanel = aNewView.associatedElement; + + // If the content area inactivated view was focused, move focus + // to the new view. + if (document.activeElement == oldView.associatedElement) + aNewView.associatedElement.focus(); + } + return aNewView; }, get currentPlace() this.currentView.place, set currentPlace(aQueryString) { let oldView = this.currentView; let newView = this.getContentViewForQueryString(aQueryString); newView.place = aQueryString; if (oldView != newView) {
--- a/browser/components/places/tests/browser/browser_410196_paste_into_tags.js +++ b/browser/components/places/tests/browser/browser_410196_paste_into_tags.js @@ -64,20 +64,17 @@ let tests = { makeHistVisit: function(aCallback) { // need to add a history object let testURI1 = NetUtil.newURI(MOZURISPEC); isnot(testURI1, null, "testURI is not null"); addVisits( {uri: testURI1, transition: PlacesUtils.history.TRANSITION_TYPED}, window, - function() { - ok(PlacesUtils.ghistory2.isVisited(testURI1), MOZURISPEC + " is a visited url."); - aCallback(); - }); + aCallback); }, makeTag: function() { // create an initial tag to work with let bmId = add_bookmark(NetUtil.newURI(TEST_URL)); ok(bmId > 0, "A bookmark was added"); PlacesUtils.tagging.tagURI(NetUtil.newURI(TEST_URL), ["foo"]); let tags = PlacesUtils.tagging.getTagsForURI(NetUtil.newURI(TEST_URL));
--- a/browser/components/places/tests/browser/browser_library_infoBox.js +++ b/browser/components/places/tests/browser/browser_library_infoBox.js @@ -17,19 +17,16 @@ var gLibrary; gTests.push({ desc: "Bug 430148 - Remove or hide the more/less button in details pane...", run: function() { var PO = gLibrary.PlacesOrganizer; let ContentTree = gLibrary.ContentTree; var infoBoxExpanderWrapper = getAndCheckElmtById("infoBoxExpanderWrapper"); function addVisitsCallback() { - var bhist = PlacesUtils.history.QueryInterface(Ci.nsIBrowserHistory); - ok(bhist.isVisited(PlacesUtils._uri(TEST_URI)), "Visit has been added."); - // open all bookmarks node PO.selectLeftPaneQuery("AllBookmarks"); isnot(PO._places.selectedNode, null, "Correctly selected all bookmarks node."); checkInfoBoxSelected(PO); ok(infoBoxExpanderWrapper.hidden, "Expander button is hidden for all bookmarks node."); checkAddInfoFieldsCollapsed(PO);
--- a/browser/components/places/tests/browser/browser_library_left_pane_commands.js +++ b/browser/components/places/tests/browser/browser_library_left_pane_commands.js @@ -14,20 +14,16 @@ var gTests = []; var gLibrary; //------------------------------------------------------------------------------ gTests.push({ desc: "Bug 489351 - Date containers under History in Library cannot be deleted/cut", run: function() { function addVisitsCallback() { - var bhist = PlacesUtils.history.QueryInterface(Ci.nsIBrowserHistory); - // Add a visit. - ok(bhist.isVisited(PlacesUtils._uri(TEST_URI)), "Visit has been added"); - // Select and open the left pane "History" query. var PO = gLibrary.PlacesOrganizer; PO.selectLeftPaneQuery('History'); isnot(PO._places.selectedNode, null, "We correctly selected History"); // Check that both delete and cut commands are disabled. ok(!PO._places.controller.isCommandEnabled("cmd_cut"), "Cut command is disabled"); @@ -51,26 +47,30 @@ gTests.push({ // Check that delete command is enabled but cut command is disabled. ok(!PO._places.controller.isCommandEnabled("cmd_cut"), "Cut command is disabled"); ok(PO._places.controller.isCommandEnabled("cmd_delete"), "Delete command is enabled"); // Execute the delete command and check visit has been removed. PO._places.controller.doCommand("cmd_delete"); - ok(!bhist.isVisited(PlacesUtils._uri(TEST_URI)), "Visit has been removed"); // Test live update of "History" query. is(historyNode.childCount, 0, "History node has no more children"); historyNode.containerOpen = false; - nextTest(); + + let testURI = NetUtil.newURI(TEST_URI); + PlacesUtils.asyncHistory.isURIVisited(testURI, function(aURI, aIsVisited) { + ok(!aIsVisited, "Visit has been removed"); + nextTest(); + }); } addVisits( - {uri: PlacesUtils._uri(TEST_URI), visitDate: Date.now() * 1000, + {uri: NetUtil.newURI(TEST_URI), visitDate: Date.now() * 1000, transition: PlacesUtils.history.TRANSITION_TYPED}, window, addVisitsCallback); } }); //------------------------------------------------------------------------------ @@ -92,17 +92,17 @@ gTests.push({ "Delete command is disabled"); var toolbarNode = PO._places.selectedNode .QueryInterface(Ci.nsINavHistoryContainerResultNode); toolbarNode.containerOpen = true; // Add an History query to the toolbar. PlacesUtils.bookmarks.insertBookmark(PlacesUtils.toolbarFolderId, - PlacesUtils._uri("place:sort=4"), + NetUtil.newURI("place:sort=4"), 0, // Insert at start. "special_query"); // Get first child and check it is the "Most Visited" smart bookmark. ok(toolbarNode.childCount > 0, "Toolbar node has children"); var queryNode = toolbarNode.getChild(0); is(queryNode.title, "special_query", "Query node is correctly selected"); // Select query node.
--- a/browser/components/sessionstore/src/SessionStore.jsm +++ b/browser/components/sessionstore/src/SessionStore.jsm @@ -3608,20 +3608,22 @@ let SessionStoreInternal = { * Bool update all windows */ saveState: function ssi_saveState(aUpdateAll) { // If crash recovery is disabled, we only want to resume with pinned tabs // if we crash. let pinnedOnly = this._loadState == STATE_RUNNING && !this._resume_from_crash; TelemetryStopwatch.start("FX_SESSION_RESTORE_COLLECT_DATA_MS"); + TelemetryStopwatch.start("FX_SESSION_RESTORE_COLLECT_DATA_LONGEST_OP_MS"); var oState = this._getCurrentState(aUpdateAll, pinnedOnly); if (!oState) { TelemetryStopwatch.cancel("FX_SESSION_RESTORE_COLLECT_DATA_MS"); + TelemetryStopwatch.cancel("FX_SESSION_RESTORE_COLLECT_DATA_LONGEST_OP_MS"); return; } // Forget about private windows. for (let i = oState.windows.length - 1; i >= 0; i--) { if (oState.windows[i].isPrivate) { oState.windows.splice(i, 1); if (oState.selectedWindow >= i) { @@ -3665,27 +3667,30 @@ let SessionStoreInternal = { } } // Persist the last session if we deferred restoring it if (this._lastSessionState) oState.lastSessionState = this._lastSessionState; TelemetryStopwatch.finish("FX_SESSION_RESTORE_COLLECT_DATA_MS"); + TelemetryStopwatch.finish("FX_SESSION_RESTORE_COLLECT_DATA_LONGEST_OP_MS"); this._saveStateObject(oState); }, /** * write a state object to disk */ _saveStateObject: function ssi_saveStateObject(aStateObj) { TelemetryStopwatch.start("FX_SESSION_RESTORE_SERIALIZE_DATA_MS"); + TelemetryStopwatch.start("FX_SESSION_RESTORE_SERIALIZE_DATA_LONGEST_OP_MS"); let data = this._toJSONString(aStateObj); TelemetryStopwatch.finish("FX_SESSION_RESTORE_SERIALIZE_DATA_MS"); + TelemetryStopwatch.finish("FX_SESSION_RESTORE_SERIALIZE_DATA_LONGEST_OP_MS"); let stateString = this._createSupportsString(data); Services.obs.notifyObservers(stateString, "sessionstore-state-write", ""); data = stateString.data; // Don't touch the file if an observer has deleted all state data. if (!data) { return;
--- a/content/base/public/nsDOMFile.h +++ b/content/base/public/nsDOMFile.h @@ -43,18 +43,16 @@ public: virtual already_AddRefed<nsIDOMBlob> CreateSlice(uint64_t aStart, uint64_t aLength, const nsAString& aContentType) = 0; virtual const nsTArray<nsCOMPtr<nsIDOMBlob> >* GetSubBlobs() const { return nullptr; } - virtual bool IsMemoryBacked() const { return false; } - NS_DECL_NSIDOMBLOB NS_DECL_NSIDOMFILE NS_DECL_NSIXHRSENDABLE NS_DECL_NSIMUTABLE void SetLazyData(const nsAString& aName, const nsAString& aContentType, uint64_t aLength, uint64_t aLastModifiedDate) @@ -347,19 +345,18 @@ protected: class nsDOMMemoryFile : public nsDOMFile { public: // Create as file nsDOMMemoryFile(void *aMemoryBuffer, uint64_t aLength, const nsAString& aName, - const nsAString& aContentType, - uint64_t aModDate = UINT64_MAX) - : nsDOMFile(aName, aContentType, aLength, aModDate), + const nsAString& aContentType) + : nsDOMFile(aName, aContentType, aLength, UINT64_MAX), mDataOwner(new DataOwner(aMemoryBuffer, aLength)) { NS_ASSERTION(mDataOwner && mDataOwner->mData, "must have data"); } // Create as blob nsDOMMemoryFile(void *aMemoryBuffer, uint64_t aLength, @@ -367,20 +364,16 @@ public: : nsDOMFile(aContentType, aLength), mDataOwner(new DataOwner(aMemoryBuffer, aLength)) { NS_ASSERTION(mDataOwner && mDataOwner->mData, "must have data"); } NS_IMETHOD GetInternalStream(nsIInputStream**); - virtual bool IsMemoryBacked() const { return true; } - void* GetData() const { return mDataOwner->mData; } - uint64_t GetLength() const { return mDataOwner->mLength; } - protected: // Create slice nsDOMMemoryFile(const nsDOMMemoryFile* aOther, uint64_t aStart, uint64_t aLength, const nsAString& aContentType) : nsDOMFile(aContentType, aOther->mStart + aStart, aLength), mDataOwner(aOther->mDataOwner) { NS_ASSERTION(mDataOwner && mDataOwner->mData, "must have data");
--- a/content/base/src/nsDOMBlobBuilder.cpp +++ b/content/base/src/nsDOMBlobBuilder.cpp @@ -141,25 +141,25 @@ nsDOMMultipartFile::CreateSlice(uint64_t nsCOMPtr<nsIDOMBlob> blob = new nsDOMMultipartFile(blobs, aContentType); return blob.forget(); } /* static */ nsresult nsDOMMultipartFile::NewFile(const nsAString& aName, nsISupports* *aNewObject) { nsCOMPtr<nsISupports> file = - do_QueryObject(new nsDOMMultipartFile(aName, EmptyString())); + do_QueryObject(new nsDOMMultipartFile(aName)); file.forget(aNewObject); return NS_OK; } /* static */ nsresult nsDOMMultipartFile::NewBlob(nsISupports* *aNewObject) { - nsCOMPtr<nsISupports> file = do_QueryObject(new nsDOMMultipartFile(EmptyString())); + nsCOMPtr<nsISupports> file = do_QueryObject(new nsDOMMultipartFile()); file.forget(aNewObject); return NS_OK; } static nsIDOMBlob* GetXPConnectNative(JSContext* aCx, JSObject* aObj) { nsCOMPtr<nsIDOMBlob> blob = do_QueryInterface( nsContentUtils::XPConnect()->GetNativeOfWrapper(aCx, aObj));
--- a/content/base/src/nsDOMBlobBuilder.h +++ b/content/base/src/nsDOMBlobBuilder.h @@ -12,42 +12,41 @@ #include "mozilla/Attributes.h" #include <algorithm> class nsDOMMultipartFile : public nsDOMFile, public nsIJSNativeInitializer { public: // Create as a file - nsDOMMultipartFile(nsTArray<nsCOMPtr<nsIDOMBlob> >& aBlobs, + nsDOMMultipartFile(nsTArray<nsCOMPtr<nsIDOMBlob> > aBlobs, const nsAString& aName, const nsAString& aContentType) : nsDOMFile(aName, aContentType, UINT64_MAX), mBlobs(aBlobs) { } // Create as a blob nsDOMMultipartFile(nsTArray<nsCOMPtr<nsIDOMBlob> >& aBlobs, const nsAString& aContentType) : nsDOMFile(aContentType, UINT64_MAX), mBlobs(aBlobs) { } // Create as a file to be later initialized - nsDOMMultipartFile(const nsAString& aName, - const nsAString& aContentType) - : nsDOMFile(aName, aContentType, UINT64_MAX) + nsDOMMultipartFile(const nsAString& aName) + : nsDOMFile(aName, EmptyString(), UINT64_MAX) { } // Create as a blob to be later initialized - nsDOMMultipartFile(const nsAString& aContentType) - : nsDOMFile(aContentType, UINT64_MAX) + nsDOMMultipartFile() + : nsDOMFile(EmptyString(), UINT64_MAX) { } NS_DECL_ISUPPORTS_INHERITED // nsIJSNativeInitializer NS_IMETHOD Initialize(nsISupports* aOwner, JSContext* aCx, @@ -85,22 +84,16 @@ public: // Initialization will set the filename, so we can pass in an empty string // for now. return NewFile(EmptyString(), aNewObject); } virtual const nsTArray<nsCOMPtr<nsIDOMBlob> >* GetSubBlobs() const { return &mBlobs; } - void - AddBlob(nsIDOMBlob* aBlob) - { - mBlobs.AppendElement(aBlob); - } - protected: nsTArray<nsCOMPtr<nsIDOMBlob> > mBlobs; }; class BlobSet { public: BlobSet() : mData(nullptr), mDataLen(0), mDataBufferLen(0)
--- a/content/html/content/src/nsHTMLMediaElement.cpp +++ b/content/html/content/src/nsHTMLMediaElement.cpp @@ -2445,49 +2445,49 @@ public: virtual void NotifyBlockingChanged(MediaStreamGraph* aGraph, Blocking aBlocked) { nsCOMPtr<nsIRunnable> event; if (aBlocked == BLOCKED) { event = NS_NewRunnableMethod(this, &StreamListener::DoNotifyBlocked); } else { event = NS_NewRunnableMethod(this, &StreamListener::DoNotifyUnblocked); } - aGraph->DispatchToMainThreadAfterStreamStateUpdate(event); + aGraph->DispatchToMainThreadAfterStreamStateUpdate(event.forget()); } virtual void NotifyFinished(MediaStreamGraph* aGraph) { nsCOMPtr<nsIRunnable> event = NS_NewRunnableMethod(this, &StreamListener::DoNotifyFinished); - aGraph->DispatchToMainThreadAfterStreamStateUpdate(event); + aGraph->DispatchToMainThreadAfterStreamStateUpdate(event.forget()); } virtual void NotifyHasCurrentData(MediaStreamGraph* aGraph, bool aHasCurrentData) { MutexAutoLock lock(mMutex); if (mDidHaveCurrentData == aHasCurrentData) return; mDidHaveCurrentData = aHasCurrentData; // Ignore the case where aHasCurrentData is false. If aHasCurrentData // changes from true to false, we don't worry about it. Video elements // preserve the last played frame anyway. if (aHasCurrentData) { nsCOMPtr<nsIRunnable> event = NS_NewRunnableMethod(this, &StreamListener::DoNotifyHaveCurrentData); - aGraph->DispatchToMainThreadAfterStreamStateUpdate(event); + aGraph->DispatchToMainThreadAfterStreamStateUpdate(event.forget()); } } virtual void NotifyOutput(MediaStreamGraph* aGraph) { MutexAutoLock lock(mMutex); if (mPendingNotifyOutput) return; mPendingNotifyOutput = true; nsCOMPtr<nsIRunnable> event = NS_NewRunnableMethod(this, &StreamListener::DoNotifyOutput); - aGraph->DispatchToMainThreadAfterStreamStateUpdate(event); + aGraph->DispatchToMainThreadAfterStreamStateUpdate(event.forget()); } private: // These fields may only be accessed on the main thread nsHTMLMediaElement* mElement; bool mHaveCurrentData; bool mBlocked;
rename from content/media/webaudio/AudioEventTimeline.h rename to content/media/AudioEventTimeline.h
new file mode 100644 --- /dev/null +++ b/content/media/AudioNodeEngine.cpp @@ -0,0 +1,71 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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/. */ + +#include "AudioNodeEngine.h" + +namespace mozilla { + +void +AllocateAudioBlock(uint32_t aChannelCount, AudioChunk* aChunk) +{ + // XXX for SIMD purposes we should do something here to make sure the + // channel buffers are 16-byte aligned. + nsRefPtr<SharedBuffer> buffer = + SharedBuffer::Create(WEBAUDIO_BLOCK_SIZE*aChannelCount*sizeof(float)); + aChunk->mDuration = WEBAUDIO_BLOCK_SIZE; + aChunk->mChannelData.SetLength(aChannelCount); + float* data = static_cast<float*>(buffer->Data()); + for (uint32_t i = 0; i < aChannelCount; ++i) { + aChunk->mChannelData[i] = data + i*WEBAUDIO_BLOCK_SIZE; + } + aChunk->mBuffer = buffer.forget(); + aChunk->mVolume = 1.0f; + aChunk->mBufferFormat = AUDIO_FORMAT_FLOAT32; +} + +void +WriteZeroesToAudioBlock(AudioChunk* aChunk, uint32_t aStart, uint32_t aLength) +{ + MOZ_ASSERT(aStart + aLength <= WEBAUDIO_BLOCK_SIZE); + if (aLength == 0) + return; + for (uint32_t i = 0; i < aChunk->mChannelData.Length(); ++i) { + memset(static_cast<float*>(const_cast<void*>(aChunk->mChannelData[i])) + aStart, + 0, aLength*sizeof(float)); + } +} + +void +AudioBlockAddChannelWithScale(const float aInput[WEBAUDIO_BLOCK_SIZE], + float aScale, + float aOutput[WEBAUDIO_BLOCK_SIZE]) +{ + if (aScale == 1.0f) { + for (uint32_t i = 0; i < WEBAUDIO_BLOCK_SIZE; ++i) { + aOutput[i] += aInput[i]; + } + } else { + for (uint32_t i = 0; i < WEBAUDIO_BLOCK_SIZE; ++i) { + aOutput[i] += aInput[i]*aScale; + } + } +} + +void +AudioBlockCopyChannelWithScale(const float aInput[WEBAUDIO_BLOCK_SIZE], + float aScale, + float aOutput[WEBAUDIO_BLOCK_SIZE]) +{ + if (aScale == 1.0f) { + memcpy(aOutput, aInput, WEBAUDIO_BLOCK_SIZE*sizeof(float)); + } else { + for (uint32_t i = 0; i < WEBAUDIO_BLOCK_SIZE; ++i) { + aOutput[i] = aInput[i]*aScale; + } + } +} + +}
new file mode 100644 --- /dev/null +++ b/content/media/AudioNodeEngine.h @@ -0,0 +1,155 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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/. */ +#ifndef MOZILLA_AUDIONODEENGINE_H_ +#define MOZILLA_AUDIONODEENGINE_H_ + +#include "AudioSegment.h" +#include "mozilla/dom/AudioParam.h" + +namespace mozilla { + +class AudioNodeStream; + +// We ensure that the graph advances in steps that are multiples of the Web +// Audio block size +const uint32_t WEBAUDIO_BLOCK_SIZE_BITS = 7; +const uint32_t WEBAUDIO_BLOCK_SIZE = 1 << WEBAUDIO_BLOCK_SIZE_BITS; + +/** + * This class holds onto a set of immutable channel buffers. The storage + * for the buffers must be malloced, but the buffer pointers and the malloc + * pointers can be different (e.g. if the buffers are contained inside + * some malloced object). + */ +class ThreadSharedFloatArrayBufferList : public ThreadSharedObject { +public: + /** + * Construct with null data. + */ + ThreadSharedFloatArrayBufferList(uint32_t aCount) + { + mContents.SetLength(aCount); + } + + struct Storage { + Storage() + { + mDataToFree = nullptr; + mSampleData = nullptr; + } + ~Storage() { free(mDataToFree); } + void* mDataToFree; + const float* mSampleData; + }; + + /** + * This can be called on any thread. + */ + uint32_t GetChannels() const { return mContents.Length(); } + /** + * This can be called on any thread. + */ + const float* GetData(uint32_t aIndex) const { return mContents[aIndex].mSampleData; } + + /** + * Call this only during initialization, before the object is handed to + * any other thread. + */ + void SetData(uint32_t aIndex, void* aDataToFree, const float* aData) + { + Storage* s = &mContents[aIndex]; + free(s->mDataToFree); + s->mDataToFree = aDataToFree; + s->mSampleData = aData; + } + + /** + * Put this object into an error state where there are no channels. + */ + void Clear() { mContents.Clear(); } + +private: + AutoFallibleTArray<Storage,2> mContents; +}; + +/** + * Allocates an AudioChunk with fresh buffers of WEBAUDIO_BLOCK_SIZE float samples. + * AudioChunk::mChannelData's entries can be cast to float* for writing. + */ +void AllocateAudioBlock(uint32_t aChannelCount, AudioChunk* aChunk); + +/** + * aChunk must have been allocated by AllocateAudioBlock. + */ +void WriteZeroesToAudioBlock(AudioChunk* aChunk, uint32_t aStart, uint32_t aLength); + +/** + * Pointwise multiply-add operation. aScale == 1.0f should be optimized. + */ +void AudioBlockAddChannelWithScale(const float aInput[WEBAUDIO_BLOCK_SIZE], + float aScale, + float aOutput[WEBAUDIO_BLOCK_SIZE]); + +/** + * Pointwise copy-scaled operation. aScale == 1.0f should be optimized. + */ +void AudioBlockCopyChannelWithScale(const float aInput[WEBAUDIO_BLOCK_SIZE], + float aScale, + float aOutput[WEBAUDIO_BLOCK_SIZE]); + +/** + * All methods of this class and its subclasses are called on the + * MediaStreamGraph thread. + */ +class AudioNodeEngine { +public: + AudioNodeEngine() {} + virtual ~AudioNodeEngine() {} + + virtual void SetStreamTimeParameter(uint32_t aIndex, TrackTicks aParam) + { + NS_ERROR("Invalid SetStreamTimeParameter index"); + } + virtual void SetDoubleParameter(uint32_t aIndex, double aParam) + { + NS_ERROR("Invalid SetDoubleParameter index"); + } + virtual void SetInt32Parameter(uint32_t aIndex, int32_t aParam) + { + NS_ERROR("Invalid SetInt32Parameter index"); + } + virtual void SetTimelineParameter(uint32_t aIndex, + const dom::AudioParamTimeline& aValue) + { + NS_ERROR("Invalid SetTimelineParameter index"); + } + virtual void SetBuffer(already_AddRefed<ThreadSharedFloatArrayBufferList> aBuffer) + { + NS_ERROR("SetBuffer called on engine that doesn't support it"); + } + + /** + * Produce the next block of audio samples, given input samples aInput + * (the mixed data for input 0). + * By default, simply returns the mixed input. + * aInput is guaranteed to have float sample format (if it has samples at all) + * and to have been resampled to IdealAudioRate(), and to have exactly + * WEBAUDIO_BLOCK_SIZE samples. + * *aFinished is set to false by the caller. If the callee sets it to true, + * we'll finish the stream and not call this again. + */ + virtual void ProduceAudioBlock(AudioNodeStream* aStream, + const AudioChunk& aInput, + AudioChunk* aOutput, + bool* aFinished) + { + *aOutput = aInput; + } +}; + +} + +#endif /* MOZILLA_AUDIONODEENGINE_H_ */
new file mode 100644 --- /dev/null +++ b/content/media/AudioNodeStream.cpp @@ -0,0 +1,290 @@ +/* -*- Mode: C++; 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/. */ + +#include "AudioNodeStream.h" + +#include "MediaStreamGraphImpl.h" +#include "AudioNodeEngine.h" + +using namespace mozilla::dom; + +namespace mozilla { + +/** + * An AudioNodeStream produces a single audio track with ID + * AUDIO_NODE_STREAM_TRACK_ID. This track has rate IdealAudioRate(). + * Each chunk in the track is a single block of WEBAUDIO_BLOCK_SIZE samples. + */ +static const int AUDIO_NODE_STREAM_TRACK_ID = 1; + +AudioNodeStream::~AudioNodeStream() +{ +} + +void +AudioNodeStream::SetStreamTimeParameter(uint32_t aIndex, MediaStream* aRelativeToStream, + double aStreamTime) +{ + class Message : public ControlMessage { + public: + Message(AudioNodeStream* aStream, uint32_t aIndex, MediaStream* aRelativeToStream, + double aStreamTime) + : ControlMessage(aStream), mStreamTime(aStreamTime), + mRelativeToStream(aRelativeToStream), mIndex(aIndex) {} + virtual void Run() + { + static_cast<AudioNodeStream*>(mStream)-> + SetStreamTimeParameterImpl(mIndex, mRelativeToStream, mStreamTime); + } + double mStreamTime; + MediaStream* mRelativeToStream; + uint32_t mIndex; + }; + + MOZ_ASSERT(this); + GraphImpl()->AppendMessage(new Message(this, aIndex, aRelativeToStream, aStreamTime)); +} + +void +AudioNodeStream::SetStreamTimeParameterImpl(uint32_t aIndex, MediaStream* aRelativeToStream, + double aStreamTime) +{ + StreamTime streamTime = std::max<MediaTime>(0, SecondsToMediaTime(aStreamTime)); + GraphTime graphTime = aRelativeToStream->StreamTimeToGraphTime(streamTime); + StreamTime thisStreamTime = GraphTimeToStreamTimeOptimistic(graphTime); + TrackTicks ticks = TimeToTicksRoundDown(IdealAudioRate(), thisStreamTime); + mEngine->SetStreamTimeParameter(aIndex, ticks); +} + +void +AudioNodeStream::SetDoubleParameter(uint32_t aIndex, double aValue) +{ + class Message : public ControlMessage { + public: + Message(AudioNodeStream* aStream, uint32_t aIndex, double aValue) + : ControlMessage(aStream), mValue(aValue), mIndex(aIndex) {} + virtual void Run() + { + static_cast<AudioNodeStream*>(mStream)->Engine()-> + SetDoubleParameter(mIndex, mValue); + } + double mValue; + uint32_t mIndex; + }; + + MOZ_ASSERT(this); + GraphImpl()->AppendMessage(new Message(this, aIndex, aValue)); +} + +void +AudioNodeStream::SetInt32Parameter(uint32_t aIndex, int32_t aValue) +{ + class Message : public ControlMessage { + public: + Message(AudioNodeStream* aStream, uint32_t aIndex, int32_t aValue) + : ControlMessage(aStream), mValue(aValue), mIndex(aIndex) {} + virtual void Run() + { + static_cast<AudioNodeStream*>(mStream)->Engine()-> + SetInt32Parameter(mIndex, mValue); + } + int32_t mValue; + uint32_t mIndex; + }; + + MOZ_ASSERT(this); + GraphImpl()->AppendMessage(new Message(this, aIndex, aValue)); +} + +void +AudioNodeStream::SetTimelineParameter(uint32_t aIndex, + const AudioEventTimeline<ErrorResult>& aValue) +{ + class Message : public ControlMessage { + public: + Message(AudioNodeStream* aStream, uint32_t aIndex, + const AudioEventTimeline<ErrorResult>& aValue) + : ControlMessage(aStream), mValue(aValue), mIndex(aIndex) {} + virtual void Run() + { + static_cast<AudioNodeStream*>(mStream)->Engine()-> + SetTimelineParameter(mIndex, mValue); + } + AudioEventTimeline<ErrorResult> mValue; + uint32_t mIndex; + }; + GraphImpl()->AppendMessage(new Message(this, aIndex, aValue)); +} + +void +AudioNodeStream::SetBuffer(already_AddRefed<ThreadSharedFloatArrayBufferList> aBuffer) +{ + class Message : public ControlMessage { + public: + Message(AudioNodeStream* aStream, + already_AddRefed<ThreadSharedFloatArrayBufferList> aBuffer) + : ControlMessage(aStream), mBuffer(aBuffer) {} + virtual void Run() + { + static_cast<AudioNodeStream*>(mStream)->Engine()-> + SetBuffer(mBuffer.forget()); + } + nsRefPtr<ThreadSharedFloatArrayBufferList> mBuffer; + }; + + MOZ_ASSERT(this); + GraphImpl()->AppendMessage(new Message(this, aBuffer)); +} + +StreamBuffer::Track* +AudioNodeStream::EnsureTrack() +{ + StreamBuffer::Track* track = mBuffer.FindTrack(AUDIO_NODE_STREAM_TRACK_ID); + if (!track) { + nsAutoPtr<MediaSegment> segment(new AudioSegment()); + for (uint32_t j = 0; j < mListeners.Length(); ++j) { + MediaStreamListener* l = mListeners[j]; + l->NotifyQueuedTrackChanges(Graph(), AUDIO_NODE_STREAM_TRACK_ID, IdealAudioRate(), 0, + MediaStreamListener::TRACK_EVENT_CREATED, + *segment); + } + track = &mBuffer.AddTrack(AUDIO_NODE_STREAM_TRACK_ID, IdealAudioRate(), 0, segment.forget()); + } + return track; +} + +AudioChunk* +AudioNodeStream::ObtainInputBlock(AudioChunk* aTmpChunk) +{ + uint32_t inputCount = mInputs.Length(); + uint32_t outputChannelCount = 0; + nsAutoTArray<AudioChunk*,250> inputChunks; + for (uint32_t i = 0; i < inputCount; ++i) { + MediaStream* s = mInputs[i]->GetSource(); + AudioNodeStream* a = s->AsAudioNodeStream(); + MOZ_ASSERT(a); + if (a->IsFinishedOnGraphThread()) { + continue; + } + AudioChunk* chunk = a->mLastChunk; + // XXX when we implement DelayNode, this will no longer be true and we'll + // need to treat a null chunk (when the DelayNode hasn't had a chance + // to produce data yet) as silence here. + MOZ_ASSERT(chunk); + if (chunk->IsNull()) { + continue; + } + + inputChunks.AppendElement(chunk); + outputChannelCount = + GetAudioChannelsSuperset(outputChannelCount, chunk->mChannelData.Length()); + } + + uint32_t inputChunkCount = inputChunks.Length(); + if (inputChunkCount == 0) { + aTmpChunk->SetNull(WEBAUDIO_BLOCK_SIZE); + return aTmpChunk; + } + + if (inputChunkCount == 1) { + return inputChunks[0]; + } + + AllocateAudioBlock(outputChannelCount, aTmpChunk); + + for (uint32_t i = 0; i < inputChunkCount; ++i) { + AudioChunk* chunk = inputChunks[i]; + nsAutoTArray<const void*,GUESS_AUDIO_CHANNELS> channels; + channels.AppendElements(chunk->mChannelData); + if (channels.Length() < outputChannelCount) { + AudioChannelsUpMix(&channels, outputChannelCount, nullptr); + NS_ASSERTION(outputChannelCount == channels.Length(), + "We called GetAudioChannelsSuperset to avoid this"); + } + + for (uint32_t c = 0; c < channels.Length(); ++c) { + const float* inputData = static_cast<const float*>(channels[c]); + float* outputData = static_cast<float*>(const_cast<void*>(aTmpChunk->mChannelData[c])); + if (inputData) { + if (i == 0) { + AudioBlockCopyChannelWithScale(inputData, chunk->mVolume, outputData); + } else { + AudioBlockAddChannelWithScale(inputData, chunk->mVolume, outputData); + } + } else { + if (i == 0) { + memset(outputData, 0, WEBAUDIO_BLOCK_SIZE*sizeof(float)); + } + } + } + } + + return aTmpChunk; +} + +// The MediaStreamGraph guarantees that this is actually one block, for +// AudioNodeStreams. +void +AudioNodeStream::ProduceOutput(GraphTime aFrom, GraphTime aTo) +{ + StreamBuffer::Track* track = EnsureTrack(); + + AudioChunk outputChunk; + AudioSegment* segment = track->Get<AudioSegment>(); + + if (mInCycle) { + // XXX DelayNode not supported yet so just produce silence + outputChunk.SetNull(WEBAUDIO_BLOCK_SIZE); + } else { + AudioChunk tmpChunk; + AudioChunk* inputChunk = ObtainInputBlock(&tmpChunk); + bool finished = false; + mEngine->ProduceAudioBlock(this, *inputChunk, &outputChunk, &finished); + if (finished) { + FinishOutput(); + } + } + + mLastChunk = segment->AppendAndConsumeChunk(&outputChunk); + + for (uint32_t j = 0; j < mListeners.Length(); ++j) { + MediaStreamListener* l = mListeners[j]; + AudioChunk copyChunk = *mLastChunk; + AudioSegment tmpSegment; + tmpSegment.AppendAndConsumeChunk(©Chunk); + l->NotifyQueuedTrackChanges(Graph(), AUDIO_NODE_STREAM_TRACK_ID, + IdealAudioRate(), segment->GetDuration(), 0, + tmpSegment); + } +} + +TrackTicks +AudioNodeStream::GetCurrentPosition() +{ + return EnsureTrack()->Get<AudioSegment>()->GetDuration(); +} + +void +AudioNodeStream::FinishOutput() +{ + if (IsFinishedOnGraphThread()) { + return; + } + + StreamBuffer::Track* track = EnsureTrack(); + track->SetEnded(); + FinishOnGraphThread(); + + for (uint32_t j = 0; j < mListeners.Length(); ++j) { + MediaStreamListener* l = mListeners[j]; + AudioSegment emptySegment; + l->NotifyQueuedTrackChanges(Graph(), AUDIO_NODE_STREAM_TRACK_ID, + IdealAudioRate(), + track->GetSegment()->GetDuration(), + MediaStreamListener::TRACK_EVENT_ENDED, emptySegment); + } +} + +}
new file mode 100644 --- /dev/null +++ b/content/media/AudioNodeStream.h @@ -0,0 +1,84 @@ +/* -*- Mode: C++; 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/. */ + +#ifndef MOZILLA_AUDIONODESTREAM_H_ +#define MOZILLA_AUDIONODESTREAM_H_ + +#include "MediaStreamGraph.h" +#include "AudioChannelFormat.h" +#include "AudioNodeEngine.h" +#include "mozilla/dom/AudioParam.h" + +#ifdef PR_LOGGING +#define LOG(type, msg) PR_LOG(gMediaStreamGraphLog, type, msg) +#else +#define LOG(type, msg) +#endif + +namespace mozilla { + +class ThreadSharedFloatArrayBufferList; + +/** + * An AudioNodeStream produces one audio track with ID AUDIO_TRACK. + * The start time of the AudioTrack is aligned to the start time of the + * AudioContext's destination node stream, plus some multiple of BLOCK_SIZE + * samples. + * + * An AudioNodeStream has an AudioNodeEngine plugged into it that does the + * actual audio processing. AudioNodeStream contains the glue code that + * integrates audio processing with the MediaStreamGraph. + */ +class AudioNodeStream : public ProcessedMediaStream { +public: + enum { AUDIO_TRACK = 1 }; + + /** + * Transfers ownership of aEngine to the new AudioNodeStream. + */ + explicit AudioNodeStream(AudioNodeEngine* aEngine) + : ProcessedMediaStream(nullptr), mEngine(aEngine), mLastChunk(nullptr) + { + } + ~AudioNodeStream(); + + // Control API + /** + * Sets a parameter that's a time relative to some stream's played time. + * This time is converted to a time relative to this stream when it's set. + */ + void SetStreamTimeParameter(uint32_t aIndex, MediaStream* aRelativeToStream, + double aStreamTime); + void SetDoubleParameter(uint32_t aIndex, double aValue); + void SetInt32Parameter(uint32_t aIndex, int32_t aValue); + void SetTimelineParameter(uint32_t aIndex, const dom::AudioParamTimeline& aValue); + void SetBuffer(already_AddRefed<ThreadSharedFloatArrayBufferList> aBuffer); + + virtual AudioNodeStream* AsAudioNodeStream() { return this; } + + // Graph thread only + void SetStreamTimeParameterImpl(uint32_t aIndex, MediaStream* aRelativeToStream, + double aStreamTime); + virtual void ProduceOutput(GraphTime aFrom, GraphTime aTo); + TrackTicks GetCurrentPosition(); + + // Any thread + AudioNodeEngine* Engine() { return mEngine; } + +protected: + void FinishOutput(); + + StreamBuffer::Track* EnsureTrack(); + AudioChunk* ObtainInputBlock(AudioChunk* aTmpChunk); + + // The engine that will generate output for this node. + nsAutoPtr<AudioNodeEngine> mEngine; + // The last block produced by this node. + AudioChunk* mLastChunk; +}; + +} + +#endif /* MOZILLA_AUDIONODESTREAM_H_ */
--- a/content/media/AudioSegment.cpp +++ b/content/media/AudioSegment.cpp @@ -84,17 +84,16 @@ void AudioSegment::ApplyVolume(float aVolume) { for (ChunkIterator ci(*this); !ci.IsEnded(); ci.Next()) { ci->mVolume *= aVolume; } } static const int AUDIO_PROCESSING_FRAMES = 640; /* > 10ms of 48KHz audio */ -static const int GUESS_AUDIO_CHANNELS = 2; static const uint8_t gZeroChannel[MAX_AUDIO_SAMPLE_SIZE*AUDIO_PROCESSING_FRAMES] = {0}; void AudioSegment::WriteTo(AudioStream* aOutput) { uint32_t outputChannels = aOutput->GetChannels(); nsAutoTArray<AudioDataValue,AUDIO_PROCESSING_FRAMES*GUESS_AUDIO_CHANNELS> buf; nsAutoTArray<const void*,GUESS_AUDIO_CHANNELS> channelData;
--- a/content/media/AudioSegment.h +++ b/content/media/AudioSegment.h @@ -11,16 +11,21 @@ #include "AudioSampleFormat.h" #include "SharedBuffer.h" namespace mozilla { class AudioStream; /** + * For auto-arrays etc, guess this as the common number of channels. + */ +const int GUESS_AUDIO_CHANNELS = 2; + +/** * An AudioChunk represents a multi-channel buffer of audio samples. * It references an underlying ThreadSharedObject which manages the lifetime * of the buffer. An AudioChunk maintains its own duration and channel data * pointers so it can represent a subinterval of a buffer without copying. * An AudioChunk can store its individual channels anywhere; it maintains * separate pointers to each channel's buffer. */ struct AudioChunk {
--- a/content/media/Makefile.in +++ b/content/media/Makefile.in @@ -14,16 +14,19 @@ LIBRARY_NAME = gkconmedia_s LIBXUL_LIBRARY = 1 ifndef _MSC_VER FAIL_ON_WARNINGS := 1 endif # !_MSC_VER EXPORTS = \ AbstractMediaDecoder.h \ AudioChannelFormat.h \ + AudioEventTimeline.h \ + AudioNodeEngine.h \ + AudioNodeStream.h \ AudioSampleFormat.h \ AudioSegment.h \ BufferMediaResource.h \ DecoderTraits.h \ FileBlockCache.h \ MediaDecoderOwner.h \ MediaResource.h \ MediaSegment.h \ @@ -41,16 +44,18 @@ EXPORTS = \ VideoUtils.h \ VideoSegment.h \ VorbisUtils.h \ MediaMetadataManager.h \ $(NULL) CPPSRCS = \ AudioChannelFormat.cpp \ + AudioNodeEngine.cpp \ + AudioNodeStream.cpp \ AudioSegment.cpp \ DecoderTraits.cpp \ FileBlockCache.cpp \ MediaResource.cpp \ MediaStreamGraph.cpp \ AudioAvailableEventManager.cpp \ MediaDecoder.cpp \ MediaDecoderStateMachine.cpp \
--- a/content/media/MediaDecoder.cpp +++ b/content/media/MediaDecoder.cpp @@ -152,40 +152,46 @@ void MediaDecoder::ConnectDecodedStreamT aStream->mPort = aStream->mStream->AllocateInputPort(mDecodedStream->mStream, MediaInputPort::FLAG_BLOCK_INPUT | MediaInputPort::FLAG_BLOCK_OUTPUT); // Unblock the output stream now. While it's connected to mDecodedStream, // mDecodedStream is responsible for controlling blocking. aStream->mStream->ChangeExplicitBlockerCount(-1); } MediaDecoder::DecodedStreamData::DecodedStreamData(MediaDecoder* aDecoder, - int64_t aInitialTime, - SourceMediaStream* aStream) + int64_t aInitialTime, + SourceMediaStream* aStream) : mLastAudioPacketTime(-1), mLastAudioPacketEndTime(-1), mAudioFramesWritten(0), mInitialTime(aInitialTime), mNextVideoTime(aInitialTime), + mDecoder(aDecoder), mStreamInitialized(false), mHaveSentFinish(false), mHaveSentFinishAudio(false), mHaveSentFinishVideo(false), mStream(aStream), - mMainThreadListener(new DecodedStreamMainThreadListener(aDecoder)), mHaveBlockedForPlayState(false) { - mStream->AddMainThreadListener(mMainThreadListener); + mStream->AddMainThreadListener(this); } MediaDecoder::DecodedStreamData::~DecodedStreamData() { - mStream->RemoveMainThreadListener(mMainThreadListener); + mStream->RemoveMainThreadListener(this); mStream->Destroy(); } +void +MediaDecoder::DecodedStreamData::NotifyMainThreadStateChanged() +{ + mDecoder->NotifyDecodedStreamMainThreadStateChanged(); +} + void MediaDecoder::DestroyDecodedStream() { MOZ_ASSERT(NS_IsMainThread()); GetReentrantMonitor().AssertCurrentThreadIn(); // All streams are having their SourceMediaStream disconnected, so they // need to be explicitly blocked again. for (int32_t i = mOutputStreams.Length() - 1; i >= 0; --i) {
--- a/content/media/MediaDecoder.h +++ b/content/media/MediaDecoder.h @@ -230,17 +230,16 @@ static const uint32_t FRAMEBUFFER_LENGTH #undef GetCurrentTime #endif class MediaDecoder : public nsIObserver, public AbstractMediaDecoder { public: typedef mozilla::layers::Image Image; - class DecodedStreamMainThreadListener; NS_DECL_ISUPPORTS NS_DECL_NSIOBSERVER // Enumeration for the valid play states (see mPlayState) enum PlayState { PLAY_STATE_START, PLAY_STATE_LOADING, @@ -335,17 +334,17 @@ public: // All MediaStream-related data is protected by mReentrantMonitor. // We have at most one DecodedStreamData per MediaDecoder. Its stream // is used as the input for each ProcessedMediaStream created by calls to // captureStream(UntilEnded). Seeking creates a new source stream, as does // replaying after the input as ended. In the latter case, the new source is // not connected to streams created by captureStreamUntilEnded. - struct DecodedStreamData { + struct DecodedStreamData MOZ_FINAL : public MainThreadMediaStreamListener { DecodedStreamData(MediaDecoder* aDecoder, int64_t aInitialTime, SourceMediaStream* aStream); ~DecodedStreamData(); // The following group of fields are protected by the decoder's monitor // and can be read or written on any thread. int64_t mLastAudioPacketTime; // microseconds int64_t mLastAudioPacketEndTime; // microseconds @@ -353,36 +352,36 @@ public: int64_t mAudioFramesWritten; // Saved value of aInitialTime. Timestamp of the first audio and/or // video packet written. int64_t mInitialTime; // microseconds // mNextVideoTime is the end timestamp for the last packet sent to the stream. // Therefore video packets starting at or after this time need to be copied // to the output stream. int64_t mNextVideoTime; // microseconds + MediaDecoder* mDecoder; // The last video image sent to the stream. Useful if we need to replicate // the image. nsRefPtr<Image> mLastVideoImage; gfxIntSize mLastVideoImageDisplaySize; // This is set to true when the stream is initialized (audio and // video tracks added). bool mStreamInitialized; bool mHaveSentFinish; bool mHaveSentFinishAudio; bool mHaveSentFinishVideo; // The decoder is responsible for calling Destroy() on this stream. // Can be read from any thread. const nsRefPtr<SourceMediaStream> mStream; - // A listener object that receives notifications when mStream's - // main-thread-visible state changes. Used on the main thread only. - const nsRefPtr<DecodedStreamMainThreadListener> mMainThreadListener; // True when we've explicitly blocked this stream because we're // not in PLAY_STATE_PLAYING. Used on the main thread only. bool mHaveBlockedForPlayState; + + virtual void NotifyMainThreadStateChanged() MOZ_OVERRIDE; }; struct OutputStreamData { void Init(ProcessedMediaStream* aStream, bool aFinishWhenEnded) { mStream = aStream; mFinishWhenEnded = aFinishWhenEnded; } nsRefPtr<ProcessedMediaStream> mStream; @@ -416,26 +415,16 @@ public: GetReentrantMonitor().AssertCurrentThreadIn(); return mOutputStreams; } DecodedStreamData* GetDecodedStream() { GetReentrantMonitor().AssertCurrentThreadIn(); return mDecodedStream; } - class DecodedStreamMainThreadListener : public MainThreadMediaStreamListener { - public: - DecodedStreamMainThreadListener(MediaDecoder* aDecoder) - : mDecoder(aDecoder) {} - virtual void NotifyMainThreadStateChanged() - { - mDecoder->NotifyDecodedStreamMainThreadStateChanged(); - } - MediaDecoder* mDecoder; - }; // Add an output stream. All decoder output will be sent to the stream. // The stream is initially blocked. The decoder is responsible for unblocking // it while it is playing back. virtual void AddOutputStream(ProcessedMediaStream* aStream, bool aFinishWhenEnded); // Return the duration of the video in seconds. virtual double GetDuration();
--- a/content/media/MediaStreamGraph.cpp +++ b/content/media/MediaStreamGraph.cpp @@ -1,520 +1,42 @@ /* -*- Mode: C++; 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/. */ -#include "MediaStreamGraph.h" +#include "MediaStreamGraphImpl.h" -#include "mozilla/Monitor.h" -#include "mozilla/TimeStamp.h" #include "AudioSegment.h" #include "VideoSegment.h" #include "nsContentUtils.h" #include "nsIAppShell.h" #include "nsIObserver.h" #include "nsServiceManagerUtils.h" #include "nsWidgetsCID.h" #include "nsXPCOMCIDInternal.h" #include "prlog.h" #include "VideoUtils.h" #include "mozilla/Attributes.h" #include "TrackUnionStream.h" #include "ImageContainer.h" #include "AudioChannelCommon.h" +#include "AudioNodeEngine.h" +#include "AudioNodeStream.h" #include <algorithm> using namespace mozilla::layers; using namespace mozilla::dom; namespace mozilla { #ifdef PR_LOGGING PRLogModuleInfo* gMediaStreamGraphLog; -#define LOG(type, msg) PR_LOG(gMediaStreamGraphLog, type, msg) -#else -#define LOG(type, msg) #endif -namespace { - -/** - * Assume we can run an iteration of the MediaStreamGraph loop in this much time - * or less. - * We try to run the control loop at this rate. - */ -const int MEDIA_GRAPH_TARGET_PERIOD_MS = 10; - -/** - * Assume that we might miss our scheduled wakeup of the MediaStreamGraph by - * this much. - */ -const int SCHEDULE_SAFETY_MARGIN_MS = 10; - -/** - * Try have this much audio buffered in streams and queued to the hardware. - * The maximum delay to the end of the next control loop - * is 2*MEDIA_GRAPH_TARGET_PERIOD_MS + SCHEDULE_SAFETY_MARGIN_MS. - * There is no point in buffering more audio than this in a stream at any - * given time (until we add processing). - * This is not optimal yet. - */ -const int AUDIO_TARGET_MS = 2*MEDIA_GRAPH_TARGET_PERIOD_MS + - SCHEDULE_SAFETY_MARGIN_MS; - -/** - * Try have this much video buffered. Video frames are set - * near the end of the iteration of the control loop. The maximum delay - * to the setting of the next video frame is 2*MEDIA_GRAPH_TARGET_PERIOD_MS + - * SCHEDULE_SAFETY_MARGIN_MS. This is not optimal yet. - */ -const int VIDEO_TARGET_MS = 2*MEDIA_GRAPH_TARGET_PERIOD_MS + - SCHEDULE_SAFETY_MARGIN_MS; - -/** - * A per-stream update message passed from the media graph thread to the - * main thread. - */ -struct StreamUpdate { - int64_t mGraphUpdateIndex; - nsRefPtr<MediaStream> mStream; - StreamTime mNextMainThreadCurrentTime; - bool mNextMainThreadFinished; -}; - -/** - * This represents a message passed from the main thread to the graph thread. - * A ControlMessage always references a particular affected stream. - */ -class ControlMessage { -public: - ControlMessage(MediaStream* aStream) : mStream(aStream) - { - MOZ_COUNT_CTOR(ControlMessage); - } - // All these run on the graph thread - virtual ~ControlMessage() - { - MOZ_COUNT_DTOR(ControlMessage); - } - // Do the action of this message on the MediaStreamGraph thread. Any actions - // affecting graph processing should take effect at mStateComputedTime. - // All stream data for times < mStateComputedTime has already been - // computed. - virtual void Run() = 0; - // When we're shutting down the application, most messages are ignored but - // some cleanup messages should still be processed (on the main thread). - virtual void RunDuringShutdown() {} - MediaStream* GetStream() { return mStream; } - -protected: - // We do not hold a reference to mStream. The graph will be holding - // a reference to the stream until the Destroy message is processed. The - // last message referencing a stream is the Destroy message for that stream. - MediaStream* mStream; -}; - -} - -/** - * The implementation of a media stream graph. This class is private to this - * file. It's not in the anonymous namespace because MediaStream needs to - * be able to friend it. - * - * Currently we only have one per process. - */ -class MediaStreamGraphImpl : public MediaStreamGraph { -public: - MediaStreamGraphImpl(); - ~MediaStreamGraphImpl() - { - NS_ASSERTION(IsEmpty(), - "All streams should have been destroyed by messages from the main thread"); - LOG(PR_LOG_DEBUG, ("MediaStreamGraph %p destroyed", this)); - } - - // Main thread only. - /** - * This runs every time we need to sync state from the media graph thread - * to the main thread while the main thread is not in the middle - * of a script. It runs during a "stable state" (per HTML5) or during - * an event posted to the main thread. - */ - void RunInStableState(); - /** - * Ensure a runnable to run RunInStableState is posted to the appshell to - * run at the next stable state (per HTML5). - * See EnsureStableStateEventPosted. - */ - void EnsureRunInStableState(); - /** - * Called to apply a StreamUpdate to its stream. - */ - void ApplyStreamUpdate(StreamUpdate* aUpdate); - /** - * Append a ControlMessage to the message queue. This queue is drained - * during RunInStableState; the messages will run on the graph thread. - */ - void AppendMessage(ControlMessage* aMessage); - /** - * Make this MediaStreamGraph enter forced-shutdown state. This state - * will be noticed by the media graph thread, which will shut down all streams - * and other state controlled by the media graph thread. - * This is called during application shutdown. - */ - void ForceShutDown(); - /** - * Shutdown() this MediaStreamGraph's threads and return when they've shut down. - */ - void ShutdownThreads(); - - // The following methods run on the graph thread (or possibly the main thread if - // mLifecycleState > LIFECYCLE_RUNNING) - /** - * Runs main control loop on the graph thread. Normally a single invocation - * of this runs for the entire lifetime of the graph thread. - */ - void RunThread(); - /** - * Call this to indicate that another iteration of the control loop is - * required on its regular schedule. The monitor must not be held. - */ - void EnsureNextIteration(); - /** - * As above, but with the monitor already held. - */ - void EnsureNextIterationLocked(MonitorAutoLock& aLock); - /** - * Call this to indicate that another iteration of the control loop is - * required immediately. The monitor must already be held. - */ - void EnsureImmediateWakeUpLocked(MonitorAutoLock& aLock); - /** - * Ensure there is an event posted to the main thread to run RunInStableState. - * mMonitor must be held. - * See EnsureRunInStableState - */ - void EnsureStableStateEventPosted(); - /** - * Generate messages to the main thread to update it for all state changes. - * mMonitor must be held. - */ - void PrepareUpdatesToMainThreadState(); - // The following methods are the various stages of RunThread processing. - /** - * Compute a new current time for the graph and advance all on-graph-thread - * state to the new current time. - */ - void UpdateCurrentTime(); - /** - * Update the consumption state of aStream to reflect whether its data - * is needed or not. - */ - void UpdateConsumptionState(SourceMediaStream* aStream); - /** - * Extract any state updates pending in aStream, and apply them. - */ - void ExtractPendingInput(SourceMediaStream* aStream, - GraphTime aDesiredUpToTime, - bool* aEnsureNextIteration); - /** - * Update "have enough data" flags in aStream. - */ - void UpdateBufferSufficiencyState(SourceMediaStream* aStream); - /* - * If aStream hasn't already been ordered, push it onto aStack and order - * its children. - */ - void UpdateStreamOrderForStream(nsTArray<MediaStream*>* aStack, - already_AddRefed<MediaStream> aStream); - /** - * Mark aStream and all its inputs (recursively) as consumed. - */ - static void MarkConsumed(MediaStream* aStream); - /** - * Sort mStreams so that every stream not in a cycle is after any streams - * it depends on, and every stream in a cycle is marked as being in a cycle. - * Also sets mIsConsumed on every stream. - */ - void UpdateStreamOrder(); - /** - * Compute the blocking states of streams from mStateComputedTime - * until the desired future time aEndBlockingDecisions. - * Updates mStateComputedTime and sets MediaStream::mBlocked - * for all streams. - */ - void RecomputeBlocking(GraphTime aEndBlockingDecisions); - // The following methods are used to help RecomputeBlocking. - /** - * If aStream isn't already in aStreams, add it and recursively call - * AddBlockingRelatedStreamsToSet on all the streams whose blocking - * status could depend on or affect the state of aStream. - */ - void AddBlockingRelatedStreamsToSet(nsTArray<MediaStream*>* aStreams, - MediaStream* aStream); - /** - * Mark a stream blocked at time aTime. If this results in decisions that need - * to be revisited at some point in the future, *aEnd will be reduced to the - * first time in the future to recompute those decisions. - */ - void MarkStreamBlocking(MediaStream* aStream); - /** - * Recompute blocking for the streams in aStreams for the interval starting at aTime. - * If this results in decisions that need to be revisited at some point - * in the future, *aEnd will be reduced to the first time in the future to - * recompute those decisions. - */ - void RecomputeBlockingAt(const nsTArray<MediaStream*>& aStreams, - GraphTime aTime, GraphTime aEndBlockingDecisions, - GraphTime* aEnd); - /** - * Returns true if aStream will underrun at aTime for its own playback. - * aEndBlockingDecisions is when we plan to stop making blocking decisions. - * *aEnd will be reduced to the first time in the future to recompute these - * decisions. - */ - bool WillUnderrun(MediaStream* aStream, GraphTime aTime, - GraphTime aEndBlockingDecisions, GraphTime* aEnd); - /** - * Given a graph time aTime, convert it to a stream time taking into - * account the time during which aStream is scheduled to be blocked. - */ - StreamTime GraphTimeToStreamTime(MediaStream* aStream, GraphTime aTime); - enum { - INCLUDE_TRAILING_BLOCKED_INTERVAL = 0x01 - }; - /** - * Given a stream time aTime, convert it to a graph time taking into - * account the time during which aStream is scheduled to be blocked. - * aTime must be <= mStateComputedTime since blocking decisions - * are only known up to that point. - * If aTime is exactly at the start of a blocked interval, then the blocked - * interval is included in the time returned if and only if - * aFlags includes INCLUDE_TRAILING_BLOCKED_INTERVAL. - */ - GraphTime StreamTimeToGraphTime(MediaStream* aStream, StreamTime aTime, - uint32_t aFlags = 0); - /** - * Get the current audio position of the stream's audio output. - */ - GraphTime GetAudioPosition(MediaStream* aStream); - /** - * Call NotifyHaveCurrentData on aStream's listeners. - */ - void NotifyHasCurrentData(MediaStream* aStream); - /** - * If aStream needs an audio stream but doesn't have one, create it. - * If aStream doesn't need an audio stream but has one, destroy it. - */ - void CreateOrDestroyAudioStreams(GraphTime aAudioOutputStartTime, - MediaStream* aStream); - /** - * Queue audio (mix of stream audio and silence for blocked intervals) - * to the audio output stream. - */ - void PlayAudio(MediaStream* aStream, GraphTime aFrom, GraphTime aTo); - /** - * Set the correct current video frame for stream aStream. - */ - void PlayVideo(MediaStream* aStream); - /** - * No more data will be forthcoming for aStream. The stream will end - * at the current buffer end point. The StreamBuffer's tracks must be - * explicitly set to finished by the caller. - */ - void FinishStream(MediaStream* aStream); - /** - * Compute how much stream data we would like to buffer for aStream. - */ - StreamTime GetDesiredBufferEnd(MediaStream* aStream); - /** - * Returns true when there are no active streams. - */ - bool IsEmpty() { return mStreams.IsEmpty() && mPortCount == 0; } - - // For use by control messages - /** - * Identify which graph update index we are currently processing. - */ - int64_t GetProcessingGraphUpdateIndex() { return mProcessingGraphUpdateIndex; } - /** - * Add aStream to the graph and initializes its graph-specific state. - */ - void AddStream(MediaStream* aStream); - /** - * Remove aStream from the graph. Ensures that pending messages about the - * stream back to the main thread are flushed. - */ - void RemoveStream(MediaStream* aStream); - /** - * Remove aPort from the graph and release it. - */ - void DestroyPort(MediaInputPort* aPort); - - // Data members - - /** - * Media graph thread. - * Readonly after initialization on the main thread. - */ - nsCOMPtr<nsIThread> mThread; - - // The following state is managed on the graph thread only, unless - // mLifecycleState > LIFECYCLE_RUNNING in which case the graph thread - // is not running and this state can be used from the main thread. - - nsTArray<nsRefPtr<MediaStream> > mStreams; - /** - * The current graph time for the current iteration of the RunThread control - * loop. - */ - GraphTime mCurrentTime; - /** - * Blocking decisions and all stream contents have been computed up to this - * time. The next batch of updates from the main thread will be processed - * at this time. Always >= mCurrentTime. - */ - GraphTime mStateComputedTime; - /** - * This is only used for logging. - */ - TimeStamp mInitialTimeStamp; - /** - * The real timestamp of the latest run of UpdateCurrentTime. - */ - TimeStamp mCurrentTimeStamp; - /** - * Which update batch we are currently processing. - */ - int64_t mProcessingGraphUpdateIndex; - /** - * Number of active MediaInputPorts - */ - int32_t mPortCount; - - // mMonitor guards the data below. - // MediaStreamGraph normally does its work without holding mMonitor, so it is - // not safe to just grab mMonitor from some thread and start monkeying with - // the graph. Instead, communicate with the graph thread using provided - // mechanisms such as the ControlMessage queue. - Monitor mMonitor; - - // Data guarded by mMonitor (must always be accessed with mMonitor held, - // regardless of the value of mLifecycleState. - - /** - * State to copy to main thread - */ - nsTArray<StreamUpdate> mStreamUpdates; - /** - * Runnables to run after the next update to main thread state. - */ - nsTArray<nsCOMPtr<nsIRunnable> > mUpdateRunnables; - struct MessageBlock { - int64_t mGraphUpdateIndex; - nsTArray<nsAutoPtr<ControlMessage> > mMessages; - }; - /** - * A list of batches of messages to process. Each batch is processed - * as an atomic unit. - */ - nsTArray<MessageBlock> mMessageQueue; - /** - * This enum specifies where this graph is in its lifecycle. This is used - * to control shutdown. - * Shutdown is tricky because it can happen in two different ways: - * 1) Shutdown due to inactivity. RunThread() detects that it has no - * pending messages and no streams, and exits. The next RunInStableState() - * checks if there are new pending messages from the main thread (true only - * if new stream creation raced with shutdown); if there are, it revives - * RunThread(), otherwise it commits to shutting down the graph. New stream - * creation after this point will create a new graph. An async event is - * dispatched to Shutdown() the graph's threads and then delete the graph - * object. - * 2) Forced shutdown at application shutdown. A flag is set, RunThread() - * detects the flag and exits, the next RunInStableState() detects the flag, - * and dispatches the async event to Shutdown() the graph's threads. However - * the graph object is not deleted. New messages for the graph are processed - * synchronously on the main thread if necessary. When the last stream is - * destroyed, the graph object is deleted. - */ - enum LifecycleState { - // The graph thread hasn't started yet. - LIFECYCLE_THREAD_NOT_STARTED, - // RunThread() is running normally. - LIFECYCLE_RUNNING, - // In the following states, the graph thread is not running so - // all "graph thread only" state in this class can be used safely - // on the main thread. - // RunThread() has exited and we're waiting for the next - // RunInStableState(), at which point we can clean up the main-thread - // side of the graph. - LIFECYCLE_WAITING_FOR_MAIN_THREAD_CLEANUP, - // RunInStableState() posted a ShutdownRunnable, and we're waiting for it - // to shut down the graph thread(s). - LIFECYCLE_WAITING_FOR_THREAD_SHUTDOWN, - // Graph threads have shut down but we're waiting for remaining streams - // to be destroyed. Only happens during application shutdown since normally - // we'd only shut down a graph when it has no streams. - LIFECYCLE_WAITING_FOR_STREAM_DESTRUCTION - }; - LifecycleState mLifecycleState; - /** - * This enum specifies the wait state of the graph thread. - */ - enum WaitState { - // RunThread() is running normally - WAITSTATE_RUNNING, - // RunThread() is paused waiting for its next iteration, which will - // happen soon - WAITSTATE_WAITING_FOR_NEXT_ITERATION, - // RunThread() is paused indefinitely waiting for something to change - WAITSTATE_WAITING_INDEFINITELY, - // Something has signaled RunThread() to wake up immediately, - // but it hasn't done so yet - WAITSTATE_WAKING_UP - }; - WaitState mWaitState; - /** - * True when another iteration of the control loop is required. - */ - bool mNeedAnotherIteration; - /** - * True when we need to do a forced shutdown during application shutdown. - */ - bool mForceShutDown; - /** - * True when we have posted an event to the main thread to run - * RunInStableState() and the event hasn't run yet. - */ - bool mPostedRunInStableStateEvent; - - // Main thread only - - /** - * Messages posted by the current event loop task. These are forwarded to - * the media graph thread during RunInStableState. We can't forward them - * immediately because we want all messages between stable states to be - * processed as an atomic batch. - */ - nsTArray<nsAutoPtr<ControlMessage> > mCurrentTaskMessageQueue; - /** - * True when RunInStableState has determined that mLifecycleState is > - * LIFECYCLE_RUNNING. Since only the main thread can reset mLifecycleState to - * LIFECYCLE_RUNNING, this can be relied on to not change unexpectedly. - */ - bool mDetectedNotRunning; - /** - * True when a stable state runner has been posted to the appshell to run - * RunInStableState at the next stable state. - */ - bool mPostedRunInStableState; -}; - /** * The singleton graph instance. */ static MediaStreamGraphImpl* gGraph; StreamTime MediaStreamGraphImpl::GetDesiredBufferEnd(MediaStream* aStream) { @@ -699,17 +221,26 @@ MediaStreamGraphImpl::GraphTimeToStreamT while (t < aTime) { GraphTime end; if (!aStream->mBlocked.GetAt(t, &end)) { s += std::min(aTime, end) - t; } t = end; } return std::max<StreamTime>(0, s); -} +} + +StreamTime +MediaStreamGraphImpl::GraphTimeToStreamTimeOptimistic(MediaStream* aStream, + GraphTime aTime) +{ + GraphTime computedUpToTime = std::min(mStateComputedTime, aTime); + StreamTime s = GraphTimeToStreamTime(aStream, computedUpToTime); + return s + (aTime - computedUpToTime); +} GraphTime MediaStreamGraphImpl::StreamTimeToGraphTime(MediaStream* aStream, StreamTime aTime, uint32_t aFlags) { if (aTime >= STREAM_TIME_MAX) { return GRAPH_TIME_MAX; } @@ -767,25 +298,17 @@ void MediaStreamGraphImpl::UpdateCurrentTime() { GraphTime prevCurrentTime = mCurrentTime; TimeStamp now = TimeStamp::Now(); GraphTime nextCurrentTime = SecondsToMediaTime((now - mCurrentTimeStamp).ToSeconds()) + mCurrentTime; if (mStateComputedTime < nextCurrentTime) { LOG(PR_LOG_WARNING, ("Media graph global underrun detected")); - LOG(PR_LOG_DEBUG, ("Advancing mStateComputedTime from %f to %f", - MediaTimeToSeconds(mStateComputedTime), - MediaTimeToSeconds(nextCurrentTime))); - // Advance mStateComputedTime to nextCurrentTime by - // adding blocked time to all streams starting at mStateComputedTime - for (uint32_t i = 0; i < mStreams.Length(); ++i) { - mStreams[i]->mBlocked.SetAtAndAfter(mStateComputedTime, true); - } - mStateComputedTime = nextCurrentTime; + nextCurrentTime = mStateComputedTime; } mCurrentTimeStamp = now; LOG(PR_LOG_DEBUG, ("Updating current time to %f (real %f, mStateComputedTime %f)", MediaTimeToSeconds(nextCurrentTime), (now - mInitialTimeStamp).ToSeconds(), MediaTimeToSeconds(mStateComputedTime))); @@ -1348,16 +871,50 @@ MediaStreamGraphImpl::EnsureNextIteratio return; mNeedAnotherIteration = true; if (mWaitState == WAITSTATE_WAITING_INDEFINITELY) { mWaitState = WAITSTATE_WAKING_UP; aLock.Notify(); } } +static GraphTime +RoundUpToAudioBlock(GraphTime aTime) +{ + TrackRate rate = IdealAudioRate(); + int64_t ticksAtIdealRate = (aTime*rate) >> MEDIA_TIME_FRAC_BITS; + // Round up to nearest block boundary + int64_t blocksAtIdealRate = + (ticksAtIdealRate + (WEBAUDIO_BLOCK_SIZE - 1)) >> + WEBAUDIO_BLOCK_SIZE_BITS; + // Round up to nearest MediaTime unit + return + ((((blocksAtIdealRate + 1)*WEBAUDIO_BLOCK_SIZE) << MEDIA_TIME_FRAC_BITS) + + rate - 1)/rate; +} + +void +MediaStreamGraphImpl::ProduceDataForStreamsBlockByBlock(uint32_t aStreamIndex, + GraphTime aFrom, + GraphTime aTo) +{ + GraphTime t = aFrom; + while (t < aTo) { + GraphTime next = RoundUpToAudioBlock(t + 1); + for (uint32_t i = aStreamIndex; i < mStreams.Length(); ++i) { + ProcessedMediaStream* ps = mStreams[i]->AsProcessedStream(); + if (ps) { + ps->ProduceOutput(t, next); + } + } + t = next; + } + NS_ASSERTION(t == aTo, "Something went wrong with rounding to block boundaries"); +} + void MediaStreamGraphImpl::RunThread() { nsTArray<MessageBlock> messageQueue; { MonitorAutoLock lock(mMonitor); messageQueue.SwapElements(mMessageQueue); } @@ -1379,19 +936,18 @@ MediaStreamGraphImpl::RunThread() for (uint32_t j = 0; j < messages.Length(); ++j) { messages[j]->Run(); } } messageQueue.Clear(); UpdateStreamOrder(); - int32_t writeAudioUpTo = AUDIO_TARGET_MS; GraphTime endBlockingDecisions = - mCurrentTime + MillisecondsToMediaTime(writeAudioUpTo); + RoundUpToAudioBlock(mCurrentTime + MillisecondsToMediaTime(AUDIO_TARGET_MS)); bool ensureNextIteration = false; // Grab pending stream input. for (uint32_t i = 0; i < mStreams.Length(); ++i) { SourceMediaStream* is = mStreams[i]->AsSourceStream(); if (is) { UpdateConsumptionState(is); ExtractPendingInput(is, endBlockingDecisions, &ensureNextIteration); @@ -1400,25 +956,37 @@ MediaStreamGraphImpl::RunThread() // Figure out which streams are blocked and when. GraphTime prevComputedTime = mStateComputedTime; RecomputeBlocking(endBlockingDecisions); // Play stream contents. uint32_t audioStreamsActive = 0; bool allBlockedForever = true; + // True when we've done ProduceOutput for all processed streams. + bool doneAllProducing = false; // Figure out what each stream wants to do for (uint32_t i = 0; i < mStreams.Length(); ++i) { MediaStream* stream = mStreams[i]; - ProcessedMediaStream* ps = stream->AsProcessedStream(); - if (ps && !ps->mFinished) { - ps->ProduceOutput(prevComputedTime, mStateComputedTime); - NS_ASSERTION(stream->mBuffer.GetEnd() >= - GraphTimeToStreamTime(stream, mStateComputedTime), - "Stream did not produce enough data"); + if (!doneAllProducing && !stream->IsFinishedOnGraphThread()) { + ProcessedMediaStream* ps = stream->AsProcessedStream(); + if (ps) { + AudioNodeStream* n = stream->AsAudioNodeStream(); + if (n) { + // Since an AudioNodeStream is present, go ahead and + // produce audio block by block for all the rest of the streams. + ProduceDataForStreamsBlockByBlock(i, prevComputedTime, mStateComputedTime); + doneAllProducing = true; + } else { + ps->ProduceOutput(prevComputedTime, mStateComputedTime); + NS_ASSERTION(stream->mBuffer.GetEnd() >= + GraphTimeToStreamTime(stream, mStateComputedTime), + "Stream did not produce enough data"); + } + } } NotifyHasCurrentData(stream); CreateOrDestroyAudioStreams(prevComputedTime, stream); PlayAudio(stream, prevComputedTime, mStateComputedTime); audioStreamsActive += stream->mAudioOutputStreams.Length(); PlayVideo(stream); SourceMediaStream* is = stream->AsSourceStream(); if (is) { @@ -1765,16 +1333,28 @@ MediaStream::Graph() } StreamTime MediaStream::GraphTimeToStreamTime(GraphTime aTime) { return GraphImpl()->GraphTimeToStreamTime(this, aTime); } +StreamTime +MediaStream::GraphTimeToStreamTimeOptimistic(GraphTime aTime) +{ + return GraphImpl()->GraphTimeToStreamTimeOptimistic(this, aTime); +} + +GraphTime +MediaStream::StreamTimeToGraphTime(StreamTime aTime) +{ + return GraphImpl()->StreamTimeToGraphTime(this, aTime, 0); +} + void MediaStream::FinishOnGraphThread() { GraphImpl()->FinishStream(this); } void MediaStream::RemoveAllListenersImpl() @@ -2211,34 +1791,37 @@ MediaInputPort::GraphImpl() } MediaStreamGraph* MediaInputPort::Graph() { return gGraph; } -MediaInputPort* +already_AddRefed<MediaInputPort> ProcessedMediaStream::AllocateInputPort(MediaStream* aStream, uint32_t aFlags) { + // This method creates two references to the MediaInputPort: one for + // the main thread, and one for the MediaStreamGraph. class Message : public ControlMessage { public: Message(MediaInputPort* aPort) : ControlMessage(aPort->GetDestination()), mPort(aPort) {} virtual void Run() { mPort->Init(); + // The graph holds its reference implicitly + mPort.forget(); } - MediaInputPort* mPort; + nsRefPtr<MediaInputPort> mPort; }; - MediaInputPort* port = new MediaInputPort(aStream, this, aFlags); - NS_ADDREF(port); + nsRefPtr<MediaInputPort> port = new MediaInputPort(aStream, this, aFlags); GraphImpl()->AppendMessage(new Message(port)); - return port; + return port.forget(); } void ProcessedMediaStream::Finish() { class Message : public ControlMessage { public: Message(ProcessedMediaStream* aStream) @@ -2255,17 +1838,17 @@ void ProcessedMediaStream::SetAutofinish(bool aAutofinish) { class Message : public ControlMessage { public: Message(ProcessedMediaStream* aStream, bool aAutofinish) : ControlMessage(aStream), mAutofinish(aAutofinish) {} virtual void Run() { - mStream->AsProcessedStream()->SetAutofinishImpl(mAutofinish); + static_cast<ProcessedMediaStream*>(mStream)->SetAutofinishImpl(mAutofinish); } bool mAutofinish; }; GraphImpl()->AppendMessage(new Message(this, aAutofinish)); } void ProcessedMediaStream::DestroyImpl() @@ -2355,9 +1938,18 @@ ProcessedMediaStream* MediaStreamGraph::CreateTrackUnionStream(nsDOMMediaStream* aWrapper) { TrackUnionStream* stream = new TrackUnionStream(aWrapper); NS_ADDREF(stream); static_cast<MediaStreamGraphImpl*>(this)->AppendMessage(new CreateMessage(stream)); return stream; } +AudioNodeStream* +MediaStreamGraph::CreateAudioNodeStream(AudioNodeEngine* aEngine) +{ + AudioNodeStream* stream = new AudioNodeStream(aEngine); + NS_ADDREF(stream); + static_cast<MediaStreamGraphImpl*>(this)->AppendMessage(new CreateMessage(stream)); + return stream; } + +}
--- a/content/media/MediaStreamGraph.h +++ b/content/media/MediaStreamGraph.h @@ -167,36 +167,36 @@ public: const MediaSegment& aQueuedMedia) {} }; /** * This is a base class for main-thread listener callbacks. * This callback is invoked on the main thread when the main-thread-visible * state of a stream has changed. * - * These methods are called without the media graph monitor held, so - * reentry into media graph methods is possible, although very much discouraged! + * These methods are called with the media graph monitor held, so + * reentry into general media graph methods is not possible. * You should do something non-blocking and non-reentrant (e.g. dispatch an - * event) and return. + * event) and return. DispatchFromMainThreadAfterNextStreamStateUpdate + * would be a good choice. * The listener is allowed to synchronously remove itself from the stream, but * not add or remove any other listeners. */ class MainThreadMediaStreamListener { public: - virtual ~MainThreadMediaStreamListener() {} - - NS_INLINE_DECL_REFCOUNTING(MainThreadMediaStreamListener) - virtual void NotifyMainThreadStateChanged() = 0; }; class MediaStreamGraphImpl; class SourceMediaStream; class ProcessedMediaStream; class MediaInputPort; +class AudioNodeStream; +class AudioNodeEngine; +struct AudioChunk; /** * A stream of synchronized audio and video data. All (not blocked) streams * progress at the same rate --- "real time". Streams cannot seek. The only * operation readers can perform on a stream is to read the next data. * * Consumers of a stream can be reading from it at different offsets, but that * should only happen due to the order in which consumers are being run. @@ -269,17 +269,22 @@ public: , mNotifiedFinished(false) , mNotifiedBlocked(false) , mWrapper(aWrapper) , mMainThreadCurrentTime(0) , mMainThreadFinished(false) , mMainThreadDestroyed(false) { } - virtual ~MediaStream() {} + virtual ~MediaStream() + { + NS_ASSERTION(mMainThreadDestroyed, "Should have been destroyed already"); + NS_ASSERTION(mMainThreadListeners.IsEmpty(), + "All main thread listeners should have been removed"); + } /** * Returns the graph that owns this stream. */ MediaStreamGraphImpl* GraphImpl(); MediaStreamGraph* Graph(); // Control API. @@ -298,21 +303,25 @@ public: void AddVideoOutput(VideoFrameContainer* aContainer); void RemoveVideoOutput(VideoFrameContainer* aContainer); // Explicitly block. Useful for example if a media element is pausing // and we need to stop its stream emitting its buffered data. void ChangeExplicitBlockerCount(int32_t aDelta); // Events will be dispatched by calling methods of aListener. void AddListener(MediaStreamListener* aListener); void RemoveListener(MediaStreamListener* aListener); + // Events will be dispatched by calling methods of aListener. It is the + // responsibility of the caller to remove aListener before it is destroyed. void AddMainThreadListener(MainThreadMediaStreamListener* aListener) { NS_ASSERTION(NS_IsMainThread(), "Call only on main thread"); mMainThreadListeners.AppendElement(aListener); } + // It's safe to call this even if aListener is not currently a listener; + // the call will be ignored. void RemoveMainThreadListener(MainThreadMediaStreamListener* aListener) { NS_ASSERTION(NS_IsMainThread(), "Call only on main thread"); mMainThreadListeners.RemoveElement(aListener); } // Signal that the client is done with this MediaStream. It will be deleted later. void Destroy(); // Returns the main-thread's view of how much data has been processed by @@ -334,16 +343,17 @@ public: return mMainThreadDestroyed; } friend class MediaStreamGraphImpl; friend class MediaInputPort; virtual SourceMediaStream* AsSourceStream() { return nullptr; } virtual ProcessedMediaStream* AsProcessedStream() { return nullptr; } + virtual AudioNodeStream* AsAudioNodeStream() { return nullptr; } // media graph thread only void Init(); // These Impl methods perform the core functionality of the control methods // above, on the media graph thread. /** * Stop all stream activity and disconnect it from all inputs and outputs. * This must be idempotent. @@ -359,17 +369,17 @@ public: void AddVideoOutputImpl(already_AddRefed<VideoFrameContainer> aContainer) { *mVideoOutputs.AppendElement() = aContainer; } void RemoveVideoOutputImpl(VideoFrameContainer* aContainer) { mVideoOutputs.RemoveElement(aContainer); } - void ChangeExplicitBlockerCountImpl(StreamTime aTime, int32_t aDelta) + void ChangeExplicitBlockerCountImpl(GraphTime aTime, int32_t aDelta) { mExplicitBlockerCount.SetAtAndAfter(aTime, mExplicitBlockerCount.GetAt(aTime) + aDelta); } void AddListenerImpl(already_AddRefed<MediaStreamListener> aListener); void RemoveListenerImpl(MediaStreamListener* aListener); void RemoveAllListenersImpl(); void AddConsumer(MediaInputPort* aPort) @@ -377,17 +387,34 @@ public: mConsumers.AppendElement(aPort); } void RemoveConsumer(MediaInputPort* aPort) { mConsumers.RemoveElement(aPort); } const StreamBuffer& GetStreamBuffer() { return mBuffer; } GraphTime GetStreamBufferStartTime() { return mBufferStartTime; } + /** + * Convert graph time to stream time. aTime must be <= mStateComputedTime + * to ensure we know exactly how much time this stream will be blocked during + * the interval. + */ StreamTime GraphTimeToStreamTime(GraphTime aTime); + /** + * Convert graph time to stream time. aTime can be > mStateComputedTime, + * in which case we optimistically assume the stream will not be blocked + * after mStateComputedTime. + */ + StreamTime GraphTimeToStreamTimeOptimistic(GraphTime aTime); + /** + * Convert stream time to graph time. The result can be > mStateComputedTime, + * in which case we did the conversion optimistically assuming the stream + * will not be blocked after mStateComputedTime. + */ + GraphTime StreamTimeToGraphTime(StreamTime aTime); bool IsFinishedOnGraphThread() { return mFinished; } void FinishOnGraphThread(); protected: virtual void AdvanceTimeVaryingValuesToCurrentTime(GraphTime aCurrentTime, GraphTime aBlockedTime) { mBufferStartTime += aBlockedTime; mGraphUpdateIndices.InsertTimeAtStart(aBlockedTime); @@ -419,17 +446,17 @@ protected: nsTArray<nsRefPtr<VideoFrameContainer> > mVideoOutputs; // We record the last played video frame to avoid redundant setting // of the current video frame. VideoFrame mLastPlayedVideoFrame; // The number of times this stream has been explicitly blocked by the control // API, minus the number of times it has been explicitly unblocked. TimeVarying<GraphTime,uint32_t> mExplicitBlockerCount; nsTArray<nsRefPtr<MediaStreamListener> > mListeners; - nsTArray<nsRefPtr<MainThreadMediaStreamListener> > mMainThreadListeners; + nsTArray<MainThreadMediaStreamListener*> mMainThreadListeners; // Precomputed blocking status (over GraphTime). // This is only valid between the graph's mCurrentTime and // mStateComputedTime. The stream is considered to have // not been blocked before mCurrentTime (its mBufferStartTime is increased // as necessary to account for that time instead) --- this avoids us having to // record the entire history of the stream's blocking-ness in mBlocked. TimeVarying<GraphTime,bool> mBlocked; @@ -738,17 +765,18 @@ public: : MediaStream(aWrapper), mAutofinish(false), mInCycle(false) {} // Control API. /** * Allocates a new input port attached to source aStream. * This stream can be removed by calling MediaInputPort::Remove(). */ - MediaInputPort* AllocateInputPort(MediaStream* aStream, uint32_t aFlags = 0); + already_AddRefed<MediaInputPort> AllocateInputPort(MediaStream* aStream, + uint32_t aFlags = 0); /** * Force this stream into the finished state. */ void Finish(); /** * Set the autofinish flag on this stream (defaults to false). When this flag * is set, and all input streams are in the finished state (including if there * are no input streams), this stream automatically enters the finished state. @@ -790,22 +818,30 @@ protected: // The list of all inputs that are currently enabled or waiting to be enabled. nsTArray<MediaInputPort*> mInputs; bool mAutofinish; // True if and only if this stream is in a cycle. // Updated by MediaStreamGraphImpl::UpdateStreamOrder. bool mInCycle; }; +// Returns ideal audio rate for processing +inline TrackRate IdealAudioRate() { return 48000; } + /** * Initially, at least, we will have a singleton MediaStreamGraph per * process. */ class MediaStreamGraph { public: + // We ensure that the graph current time advances in multiples of + // IdealAudioBlockSize()/IdealAudioRate(). A stream that never blocks + // and has a track with the ideal audio rate will produce audio in + // multiples of the block size. + // Main thread only static MediaStreamGraph* GetInstance(); // Control API. /** * Create a stream that a media decoder (or some other source of * media data, such as a camera) can write to. */ SourceMediaStream* CreateSourceStream(nsDOMMediaStream* aWrapper); @@ -820,31 +856,36 @@ public: * For each added track, the track ID of the output track is the track ID * of the input track or one plus the maximum ID of all previously added * tracks, whichever is greater. * TODO at some point we will probably need to add API to select * particular tracks of each input stream. */ ProcessedMediaStream* CreateTrackUnionStream(nsDOMMediaStream* aWrapper); /** + * Create a stream that will process audio for an AudioNode. + * Takes ownership of aEngine. + */ + AudioNodeStream* CreateAudioNodeStream(AudioNodeEngine* aEngine); + /** * Returns the number of graph updates sent. This can be used to track * whether a given update has been processed by the graph thread and reflected * in main-thread stream state. */ int64_t GetCurrentGraphUpdateIndex() { return mGraphUpdatesSent; } /** * Media graph thread only. * Dispatches a runnable that will run on the main thread after all * main-thread stream state has been next updated. * Should only be called during MediaStreamListener callbacks. */ - void DispatchToMainThreadAfterStreamStateUpdate(nsIRunnable* aRunnable) + void DispatchToMainThreadAfterStreamStateUpdate(already_AddRefed<nsIRunnable> aRunnable) { - mPendingUpdateRunnables.AppendElement(aRunnable); + *mPendingUpdateRunnables.AppendElement() = aRunnable; } protected: MediaStreamGraph() : mGraphUpdatesSent(1) { MOZ_COUNT_CTOR(MediaStreamGraph); }
copy from content/media/MediaStreamGraph.cpp copy to content/media/MediaStreamGraphImpl.h --- a/content/media/MediaStreamGraph.cpp +++ b/content/media/MediaStreamGraphImpl.h @@ -1,78 +1,63 @@ /* -*- Mode: C++; 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/. */ +#ifndef MOZILLA_MEDIASTREAMGRAPHIMPL_H_ +#define MOZILLA_MEDIASTREAMGRAPHIMPL_H_ + #include "MediaStreamGraph.h" #include "mozilla/Monitor.h" #include "mozilla/TimeStamp.h" -#include "AudioSegment.h" -#include "VideoSegment.h" -#include "nsContentUtils.h" -#include "nsIAppShell.h" -#include "nsIObserver.h" -#include "nsServiceManagerUtils.h" -#include "nsWidgetsCID.h" -#include "nsXPCOMCIDInternal.h" -#include "prlog.h" -#include "VideoUtils.h" -#include "mozilla/Attributes.h" -#include "TrackUnionStream.h" -#include "ImageContainer.h" -#include "AudioChannelCommon.h" -#include <algorithm> - -using namespace mozilla::layers; -using namespace mozilla::dom; +#include "nsIThread.h" +#include "nsIRunnable.h" namespace mozilla { #ifdef PR_LOGGING -PRLogModuleInfo* gMediaStreamGraphLog; +extern PRLogModuleInfo* gMediaStreamGraphLog; #define LOG(type, msg) PR_LOG(gMediaStreamGraphLog, type, msg) #else #define LOG(type, msg) #endif -namespace { - /** * Assume we can run an iteration of the MediaStreamGraph loop in this much time * or less. * We try to run the control loop at this rate. */ -const int MEDIA_GRAPH_TARGET_PERIOD_MS = 10; +static const int MEDIA_GRAPH_TARGET_PERIOD_MS = 10; /** * Assume that we might miss our scheduled wakeup of the MediaStreamGraph by * this much. */ -const int SCHEDULE_SAFETY_MARGIN_MS = 10; +static const int SCHEDULE_SAFETY_MARGIN_MS = 10; /** * Try have this much audio buffered in streams and queued to the hardware. * The maximum delay to the end of the next control loop * is 2*MEDIA_GRAPH_TARGET_PERIOD_MS + SCHEDULE_SAFETY_MARGIN_MS. * There is no point in buffering more audio than this in a stream at any * given time (until we add processing). * This is not optimal yet. */ -const int AUDIO_TARGET_MS = 2*MEDIA_GRAPH_TARGET_PERIOD_MS + +static const int AUDIO_TARGET_MS = 2*MEDIA_GRAPH_TARGET_PERIOD_MS + SCHEDULE_SAFETY_MARGIN_MS; /** * Try have this much video buffered. Video frames are set * near the end of the iteration of the control loop. The maximum delay * to the setting of the next video frame is 2*MEDIA_GRAPH_TARGET_PERIOD_MS + * SCHEDULE_SAFETY_MARGIN_MS. This is not optimal yet. */ -const int VIDEO_TARGET_MS = 2*MEDIA_GRAPH_TARGET_PERIOD_MS + +static const int VIDEO_TARGET_MS = 2*MEDIA_GRAPH_TARGET_PERIOD_MS + SCHEDULE_SAFETY_MARGIN_MS; /** * A per-stream update message passed from the media graph thread to the * main thread. */ struct StreamUpdate { int64_t mGraphUpdateIndex; @@ -108,18 +93,16 @@ public: protected: // We do not hold a reference to mStream. The graph will be holding // a reference to the stream until the Destroy message is processed. The // last message referencing a stream is the Destroy message for that stream. MediaStream* mStream; }; -} - /** * The implementation of a media stream graph. This class is private to this * file. It's not in the anonymous namespace because MediaStream needs to * be able to friend it. * * Currently we only have one per process. */ class MediaStreamGraphImpl : public MediaStreamGraph { @@ -262,28 +245,47 @@ public: * If this results in decisions that need to be revisited at some point * in the future, *aEnd will be reduced to the first time in the future to * recompute those decisions. */ void RecomputeBlockingAt(const nsTArray<MediaStream*>& aStreams, GraphTime aTime, GraphTime aEndBlockingDecisions, GraphTime* aEnd); /** + * Produce data for all streams >= aStreamIndex for the given time interval. + * Advances block by block, each iteration producing data for all streams + * for a single block. + * This is needed if there are WebAudio delay nodes, whose output for a block + * may depend on the output of any other node (including itself) for the + * previous block. This is probably also more performant due to better memory + * locality. + * This is called whenever we have an AudioNodeStream in the graph. + */ + void ProduceDataForStreamsBlockByBlock(uint32_t aStreamIndex, + GraphTime aFrom, + GraphTime aTo); + /** * Returns true if aStream will underrun at aTime for its own playback. * aEndBlockingDecisions is when we plan to stop making blocking decisions. * *aEnd will be reduced to the first time in the future to recompute these * decisions. */ bool WillUnderrun(MediaStream* aStream, GraphTime aTime, GraphTime aEndBlockingDecisions, GraphTime* aEnd); /** * Given a graph time aTime, convert it to a stream time taking into * account the time during which aStream is scheduled to be blocked. */ StreamTime GraphTimeToStreamTime(MediaStream* aStream, GraphTime aTime); + /** + * Given a graph time aTime, convert it to a stream time taking into + * account the time during which aStream is scheduled to be blocked, and + * when we don't know whether it's blocked or not, we assume it's not blocked. + */ + StreamTime GraphTimeToStreamTimeOptimistic(MediaStream* aStream, GraphTime aTime); enum { INCLUDE_TRAILING_BLOCKED_INTERVAL = 0x01 }; /** * Given a stream time aTime, convert it to a graph time taking into * account the time during which aStream is scheduled to be blocked. * aTime must be <= mStateComputedTime since blocking decisions * are only known up to that point. @@ -505,1859 +507,11 @@ public: bool mDetectedNotRunning; /** * True when a stable state runner has been posted to the appshell to run * RunInStableState at the next stable state. */ bool mPostedRunInStableState; }; -/** - * The singleton graph instance. - */ -static MediaStreamGraphImpl* gGraph; - -StreamTime -MediaStreamGraphImpl::GetDesiredBufferEnd(MediaStream* aStream) -{ - StreamTime current = mCurrentTime - aStream->mBufferStartTime; - return current + - MillisecondsToMediaTime(std::max(AUDIO_TARGET_MS, VIDEO_TARGET_MS)); -} - -void -MediaStreamGraphImpl::FinishStream(MediaStream* aStream) -{ - if (aStream->mFinished) - return; - LOG(PR_LOG_DEBUG, ("MediaStream %p will finish", aStream)); - aStream->mFinished = true; - // Force at least one more iteration of the control loop, since we rely - // on UpdateCurrentTime to notify our listeners once the stream end - // has been reached. - EnsureNextIteration(); -} - -void -MediaStreamGraphImpl::AddStream(MediaStream* aStream) -{ - aStream->mBufferStartTime = mCurrentTime; - *mStreams.AppendElement() = already_AddRefed<MediaStream>(aStream); - LOG(PR_LOG_DEBUG, ("Adding media stream %p to the graph", aStream)); -} - -void -MediaStreamGraphImpl::RemoveStream(MediaStream* aStream) -{ - // Remove references in mStreamUpdates before we allow aStream to die. - // Pending updates are not needed (since the main thread has already given - // up the stream) so we will just drop them. - { - MonitorAutoLock lock(mMonitor); - for (uint32_t i = 0; i < mStreamUpdates.Length(); ++i) { - if (mStreamUpdates[i].mStream == aStream) { - mStreamUpdates[i].mStream = nullptr; - } - } - } - - // This unrefs the stream, probably destroying it - mStreams.RemoveElement(aStream); - - LOG(PR_LOG_DEBUG, ("Removing media stream %p from the graph", aStream)); -} - -void -MediaStreamGraphImpl::UpdateConsumptionState(SourceMediaStream* aStream) -{ - MediaStreamListener::Consumption state = - aStream->mIsConsumed ? MediaStreamListener::CONSUMED - : MediaStreamListener::NOT_CONSUMED; - if (state != aStream->mLastConsumptionState) { - aStream->mLastConsumptionState = state; - for (uint32_t j = 0; j < aStream->mListeners.Length(); ++j) { - MediaStreamListener* l = aStream->mListeners[j]; - l->NotifyConsumptionChanged(this, state); - } - } -} - -void -MediaStreamGraphImpl::ExtractPendingInput(SourceMediaStream* aStream, - GraphTime aDesiredUpToTime, - bool* aEnsureNextIteration) -{ - bool finished; - { - MutexAutoLock lock(aStream->mMutex); - if (aStream->mPullEnabled && !aStream->mFinished && - !aStream->mListeners.IsEmpty()) { - // Compute how much stream time we'll need assuming we don't block - // the stream at all between mBlockingDecisionsMadeUntilTime and - // aDesiredUpToTime. - StreamTime t = - GraphTimeToStreamTime(aStream, mStateComputedTime) + - (aDesiredUpToTime - mStateComputedTime); - if (t > aStream->mBuffer.GetEnd()) { - *aEnsureNextIteration = true; - for (uint32_t j = 0; j < aStream->mListeners.Length(); ++j) { - MediaStreamListener* l = aStream->mListeners[j]; - { - MutexAutoUnlock unlock(aStream->mMutex); - l->NotifyPull(this, t); - } - } - } - } - finished = aStream->mUpdateFinished; - for (int32_t i = aStream->mUpdateTracks.Length() - 1; i >= 0; --i) { - SourceMediaStream::TrackData* data = &aStream->mUpdateTracks[i]; - for (uint32_t j = 0; j < aStream->mListeners.Length(); ++j) { - MediaStreamListener* l = aStream->mListeners[j]; - TrackTicks offset = (data->mCommands & SourceMediaStream::TRACK_CREATE) - ? data->mStart : aStream->mBuffer.FindTrack(data->mID)->GetSegment()->GetDuration(); - l->NotifyQueuedTrackChanges(this, data->mID, data->mRate, - offset, data->mCommands, *data->mData); - } - if (data->mCommands & SourceMediaStream::TRACK_CREATE) { - MediaSegment* segment = data->mData.forget(); - LOG(PR_LOG_DEBUG, ("SourceMediaStream %p creating track %d, rate %d, start %lld, initial end %lld", - aStream, data->mID, data->mRate, int64_t(data->mStart), - int64_t(segment->GetDuration()))); - aStream->mBuffer.AddTrack(data->mID, data->mRate, data->mStart, segment); - // The track has taken ownership of data->mData, so let's replace - // data->mData with an empty clone. - data->mData = segment->CreateEmptyClone(); - data->mCommands &= ~SourceMediaStream::TRACK_CREATE; - } else if (data->mData->GetDuration() > 0) { - MediaSegment* dest = aStream->mBuffer.FindTrack(data->mID)->GetSegment(); - LOG(PR_LOG_DEBUG, ("SourceMediaStream %p track %d, advancing end from %lld to %lld", - aStream, data->mID, - int64_t(dest->GetDuration()), - int64_t(dest->GetDuration() + data->mData->GetDuration()))); - dest->AppendFrom(data->mData); - } - if (data->mCommands & SourceMediaStream::TRACK_END) { - aStream->mBuffer.FindTrack(data->mID)->SetEnded(); - aStream->mUpdateTracks.RemoveElementAt(i); - } - } - aStream->mBuffer.AdvanceKnownTracksTime(aStream->mUpdateKnownTracksTime); - } - if (finished) { - FinishStream(aStream); - } -} - -void -MediaStreamGraphImpl::UpdateBufferSufficiencyState(SourceMediaStream* aStream) -{ - StreamTime desiredEnd = GetDesiredBufferEnd(aStream); - nsTArray<SourceMediaStream::ThreadAndRunnable> runnables; - - { - MutexAutoLock lock(aStream->mMutex); - for (uint32_t i = 0; i < aStream->mUpdateTracks.Length(); ++i) { - SourceMediaStream::TrackData* data = &aStream->mUpdateTracks[i]; - if (data->mCommands & SourceMediaStream::TRACK_CREATE) { - // This track hasn't been created yet, so we have no sufficiency - // data. The track will be created in the next iteration of the - // control loop and then we'll fire insufficiency notifications - // if necessary. - continue; - } - if (data->mCommands & SourceMediaStream::TRACK_END) { - // This track will end, so no point in firing not-enough-data - // callbacks. - continue; - } - StreamBuffer::Track* track = aStream->mBuffer.FindTrack(data->mID); - // Note that track->IsEnded() must be false, otherwise we would have - // removed the track from mUpdateTracks already. - NS_ASSERTION(!track->IsEnded(), "What is this track doing here?"); - data->mHaveEnough = track->GetEndTimeRoundDown() >= desiredEnd; - if (!data->mHaveEnough) { - runnables.MoveElementsFrom(data->mDispatchWhenNotEnough); - } - } - } - - for (uint32_t i = 0; i < runnables.Length(); ++i) { - runnables[i].mThread->Dispatch(runnables[i].mRunnable, 0); - } -} - -StreamTime -MediaStreamGraphImpl::GraphTimeToStreamTime(MediaStream* aStream, - GraphTime aTime) -{ - NS_ASSERTION(aTime <= mStateComputedTime, - "Don't ask about times where we haven't made blocking decisions yet"); - if (aTime <= mCurrentTime) { - return std::max<StreamTime>(0, aTime - aStream->mBufferStartTime); - } - GraphTime t = mCurrentTime; - StreamTime s = t - aStream->mBufferStartTime; - while (t < aTime) { - GraphTime end; - if (!aStream->mBlocked.GetAt(t, &end)) { - s += std::min(aTime, end) - t; - } - t = end; - } - return std::max<StreamTime>(0, s); -} - -GraphTime -MediaStreamGraphImpl::StreamTimeToGraphTime(MediaStream* aStream, - StreamTime aTime, uint32_t aFlags) -{ - if (aTime >= STREAM_TIME_MAX) { - return GRAPH_TIME_MAX; - } - MediaTime bufferElapsedToCurrentTime = mCurrentTime - aStream->mBufferStartTime; - if (aTime < bufferElapsedToCurrentTime || - (aTime == bufferElapsedToCurrentTime && !(aFlags & INCLUDE_TRAILING_BLOCKED_INTERVAL))) { - return aTime + aStream->mBufferStartTime; - } - - MediaTime streamAmount = aTime - bufferElapsedToCurrentTime; - NS_ASSERTION(streamAmount >= 0, "Can't answer queries before current time"); - - GraphTime t = mCurrentTime; - while (t < GRAPH_TIME_MAX) { - bool blocked; - GraphTime end; - if (t < mStateComputedTime) { - blocked = aStream->mBlocked.GetAt(t, &end); - end = std::min(end, mStateComputedTime); - } else { - blocked = false; - end = GRAPH_TIME_MAX; - } - if (blocked) { - t = end; - } else { - if (streamAmount == 0) { - // No more stream time to consume at time t, so we're done. - break; - } - MediaTime consume = std::min(end - t, streamAmount); - streamAmount -= consume; - t += consume; - } - } - return t; -} - -GraphTime -MediaStreamGraphImpl::GetAudioPosition(MediaStream* aStream) -{ - if (aStream->mAudioOutputStreams.IsEmpty()) { - return mCurrentTime; - } - int64_t positionInFrames = aStream->mAudioOutputStreams[0].mStream->GetPositionInFrames(); - if (positionInFrames < 0) { - return mCurrentTime; - } - return aStream->mAudioOutputStreams[0].mAudioPlaybackStartTime + - TicksToTimeRoundDown(aStream->mAudioOutputStreams[0].mStream->GetRate(), - positionInFrames); -} - -void -MediaStreamGraphImpl::UpdateCurrentTime() -{ - GraphTime prevCurrentTime = mCurrentTime; - TimeStamp now = TimeStamp::Now(); - GraphTime nextCurrentTime = - SecondsToMediaTime((now - mCurrentTimeStamp).ToSeconds()) + mCurrentTime; - if (mStateComputedTime < nextCurrentTime) { - LOG(PR_LOG_WARNING, ("Media graph global underrun detected")); - LOG(PR_LOG_DEBUG, ("Advancing mStateComputedTime from %f to %f", - MediaTimeToSeconds(mStateComputedTime), - MediaTimeToSeconds(nextCurrentTime))); - // Advance mStateComputedTime to nextCurrentTime by - // adding blocked time to all streams starting at mStateComputedTime - for (uint32_t i = 0; i < mStreams.Length(); ++i) { - mStreams[i]->mBlocked.SetAtAndAfter(mStateComputedTime, true); - } - mStateComputedTime = nextCurrentTime; - } - mCurrentTimeStamp = now; - - LOG(PR_LOG_DEBUG, ("Updating current time to %f (real %f, mStateComputedTime %f)", - MediaTimeToSeconds(nextCurrentTime), - (now - mInitialTimeStamp).ToSeconds(), - MediaTimeToSeconds(mStateComputedTime))); - - if (prevCurrentTime >= nextCurrentTime) { - NS_ASSERTION(prevCurrentTime == nextCurrentTime, "Time can't go backwards!"); - // This could happen due to low clock resolution, maybe? - LOG(PR_LOG_DEBUG, ("Time did not advance")); - // There's not much left to do here, but the code below that notifies - // listeners that streams have ended still needs to run. - } - - for (uint32_t i = 0; i < mStreams.Length(); ++i) { - MediaStream* stream = mStreams[i]; - - // Calculate blocked time and fire Blocked/Unblocked events - GraphTime blockedTime = 0; - GraphTime t = prevCurrentTime; - while (t < nextCurrentTime) { - GraphTime end; - bool blocked = stream->mBlocked.GetAt(t, &end); - if (blocked) { - blockedTime += std::min(end, nextCurrentTime) - t; - } - if (blocked != stream->mNotifiedBlocked) { - for (uint32_t j = 0; j < stream->mListeners.Length(); ++j) { - MediaStreamListener* l = stream->mListeners[j]; - l->NotifyBlockingChanged(this, - blocked ? MediaStreamListener::BLOCKED : MediaStreamListener::UNBLOCKED); - } - stream->mNotifiedBlocked = blocked; - } - t = end; - } - - stream->AdvanceTimeVaryingValuesToCurrentTime(nextCurrentTime, blockedTime); - // Advance mBlocked last so that implementations of - // AdvanceTimeVaryingValuesToCurrentTime can rely on the value of mBlocked. - stream->mBlocked.AdvanceCurrentTime(nextCurrentTime); - - if (blockedTime < nextCurrentTime - prevCurrentTime) { - for (uint32_t i = 0; i < stream->mListeners.Length(); ++i) { - MediaStreamListener* l = stream->mListeners[i]; - l->NotifyOutput(this); - } - } - - if (stream->mFinished && !stream->mNotifiedFinished && - stream->mBufferStartTime + stream->GetBufferEnd() <= nextCurrentTime) { - stream->mNotifiedFinished = true; - stream->mLastPlayedVideoFrame.SetNull(); - for (uint32_t j = 0; j < stream->mListeners.Length(); ++j) { - MediaStreamListener* l = stream->mListeners[j]; - l->NotifyFinished(this); - } - } - - LOG(PR_LOG_DEBUG, ("MediaStream %p bufferStartTime=%f blockedTime=%f", - stream, MediaTimeToSeconds(stream->mBufferStartTime), - MediaTimeToSeconds(blockedTime))); - } - - mCurrentTime = nextCurrentTime; -} - -bool -MediaStreamGraphImpl::WillUnderrun(MediaStream* aStream, GraphTime aTime, - GraphTime aEndBlockingDecisions, GraphTime* aEnd) -{ - // Finished streams can't underrun. ProcessedMediaStreams also can't cause - // underrun currently, since we'll always be able to produce data for them - // unless they block on some other stream. - if (aStream->mFinished || aStream->AsProcessedStream()) { - return false; - } - GraphTime bufferEnd = - StreamTimeToGraphTime(aStream, aStream->GetBufferEnd(), - INCLUDE_TRAILING_BLOCKED_INTERVAL); - NS_ASSERTION(bufferEnd >= mCurrentTime, "Buffer underran"); - // We should block after bufferEnd. - if (bufferEnd <= aTime) { - LOG(PR_LOG_DEBUG, ("MediaStream %p will block due to data underrun, " - "bufferEnd %f", - aStream, MediaTimeToSeconds(bufferEnd))); - return true; - } - // We should keep blocking if we're currently blocked and we don't have - // data all the way through to aEndBlockingDecisions. If we don't have - // data all the way through to aEndBlockingDecisions, we'll block soon, - // but we might as well remain unblocked and play the data we've got while - // we can. - if (bufferEnd <= aEndBlockingDecisions && aStream->mBlocked.GetBefore(aTime)) { - LOG(PR_LOG_DEBUG, ("MediaStream %p will block due to speculative data underrun, " - "bufferEnd %f", - aStream, MediaTimeToSeconds(bufferEnd))); - return true; - } - // Reconsider decisions at bufferEnd - *aEnd = std::min(*aEnd, bufferEnd); - return false; -} - -void -MediaStreamGraphImpl::MarkConsumed(MediaStream* aStream) -{ - if (aStream->mIsConsumed) { - return; - } - aStream->mIsConsumed = true; - - ProcessedMediaStream* ps = aStream->AsProcessedStream(); - if (!ps) { - return; - } - // Mark all the inputs to this stream as consumed - for (uint32_t i = 0; i < ps->mInputs.Length(); ++i) { - MarkConsumed(ps->mInputs[i]->mSource); - } -} - -void -MediaStreamGraphImpl::UpdateStreamOrderForStream(nsTArray<MediaStream*>* aStack, - already_AddRefed<MediaStream> aStream) -{ - nsRefPtr<MediaStream> stream = aStream; - NS_ASSERTION(!stream->mHasBeenOrdered, "stream should not have already been ordered"); - if (stream->mIsOnOrderingStack) { - for (int32_t i = aStack->Length() - 1; ; --i) { - aStack->ElementAt(i)->AsProcessedStream()->mInCycle = true; - if (aStack->ElementAt(i) == stream) - break; - } - return; - } - ProcessedMediaStream* ps = stream->AsProcessedStream(); - if (ps) { - aStack->AppendElement(stream); - stream->mIsOnOrderingStack = true; - for (uint32_t i = 0; i < ps->mInputs.Length(); ++i) { - MediaStream* source = ps->mInputs[i]->mSource; - if (!source->mHasBeenOrdered) { - nsRefPtr<MediaStream> s = source; - UpdateStreamOrderForStream(aStack, s.forget()); - } - } - aStack->RemoveElementAt(aStack->Length() - 1); - stream->mIsOnOrderingStack = false; - } - - stream->mHasBeenOrdered = true; - *mStreams.AppendElement() = stream.forget(); -} - -void -MediaStreamGraphImpl::UpdateStreamOrder() -{ - nsTArray<nsRefPtr<MediaStream> > oldStreams; - oldStreams.SwapElements(mStreams); - for (uint32_t i = 0; i < oldStreams.Length(); ++i) { - MediaStream* stream = oldStreams[i]; - stream->mHasBeenOrdered = false; - stream->mIsConsumed = false; - stream->mIsOnOrderingStack = false; - stream->mInBlockingSet = false; - ProcessedMediaStream* ps = stream->AsProcessedStream(); - if (ps) { - ps->mInCycle = false; - } - } - - nsAutoTArray<MediaStream*,10> stack; - for (uint32_t i = 0; i < oldStreams.Length(); ++i) { - nsRefPtr<MediaStream>& s = oldStreams[i]; - if (!s->mAudioOutputs.IsEmpty() || !s->mVideoOutputs.IsEmpty()) { - MarkConsumed(s); - } - if (!s->mHasBeenOrdered) { - UpdateStreamOrderForStream(&stack, s.forget()); - } - } -} - -void -MediaStreamGraphImpl::RecomputeBlocking(GraphTime aEndBlockingDecisions) -{ - bool blockingDecisionsWillChange = false; - - LOG(PR_LOG_DEBUG, ("Media graph %p computing blocking for time %f", - this, MediaTimeToSeconds(mStateComputedTime))); - for (uint32_t i = 0; i < mStreams.Length(); ++i) { - MediaStream* stream = mStreams[i]; - if (!stream->mInBlockingSet) { - // Compute a partition of the streams containing 'stream' such that we can - // compute the blocking status of each subset independently. - nsAutoTArray<MediaStream*,10> streamSet; - AddBlockingRelatedStreamsToSet(&streamSet, stream); - - GraphTime end; - for (GraphTime t = mStateComputedTime; - t < aEndBlockingDecisions; t = end) { - end = GRAPH_TIME_MAX; - RecomputeBlockingAt(streamSet, t, aEndBlockingDecisions, &end); - if (end < GRAPH_TIME_MAX) { - blockingDecisionsWillChange = true; - } - } - } - - GraphTime end; - stream->mBlocked.GetAt(mCurrentTime, &end); - if (end < GRAPH_TIME_MAX) { - blockingDecisionsWillChange = true; - } - } - LOG(PR_LOG_DEBUG, ("Media graph %p computed blocking for interval %f to %f", - this, MediaTimeToSeconds(mStateComputedTime), - MediaTimeToSeconds(aEndBlockingDecisions))); - mStateComputedTime = aEndBlockingDecisions; - - if (blockingDecisionsWillChange) { - // Make sure we wake up to notify listeners about these changes. - EnsureNextIteration(); - } -} - -void -MediaStreamGraphImpl::AddBlockingRelatedStreamsToSet(nsTArray<MediaStream*>* aStreams, - MediaStream* aStream) -{ - if (aStream->mInBlockingSet) - return; - aStream->mInBlockingSet = true; - aStreams->AppendElement(aStream); - for (uint32_t i = 0; i < aStream->mConsumers.Length(); ++i) { - MediaInputPort* port = aStream->mConsumers[i]; - if (port->mFlags & (MediaInputPort::FLAG_BLOCK_INPUT | MediaInputPort::FLAG_BLOCK_OUTPUT)) { - AddBlockingRelatedStreamsToSet(aStreams, port->mDest); - } - } - ProcessedMediaStream* ps = aStream->AsProcessedStream(); - if (ps) { - for (uint32_t i = 0; i < ps->mInputs.Length(); ++i) { - MediaInputPort* port = ps->mInputs[i]; - if (port->mFlags & (MediaInputPort::FLAG_BLOCK_INPUT | MediaInputPort::FLAG_BLOCK_OUTPUT)) { - AddBlockingRelatedStreamsToSet(aStreams, port->mSource); - } - } - } -} - -void -MediaStreamGraphImpl::MarkStreamBlocking(MediaStream* aStream) -{ - if (aStream->mBlockInThisPhase) - return; - aStream->mBlockInThisPhase = true; - for (uint32_t i = 0; i < aStream->mConsumers.Length(); ++i) { - MediaInputPort* port = aStream->mConsumers[i]; - if (port->mFlags & MediaInputPort::FLAG_BLOCK_OUTPUT) { - MarkStreamBlocking(port->mDest); - } - } - ProcessedMediaStream* ps = aStream->AsProcessedStream(); - if (ps) { - for (uint32_t i = 0; i < ps->mInputs.Length(); ++i) { - MediaInputPort* port = ps->mInputs[i]; - if (port->mFlags & MediaInputPort::FLAG_BLOCK_INPUT) { - MarkStreamBlocking(port->mSource); - } - } - } -} - -void -MediaStreamGraphImpl::RecomputeBlockingAt(const nsTArray<MediaStream*>& aStreams, - GraphTime aTime, - GraphTime aEndBlockingDecisions, - GraphTime* aEnd) -{ - for (uint32_t i = 0; i < aStreams.Length(); ++i) { - MediaStream* stream = aStreams[i]; - stream->mBlockInThisPhase = false; - } - - for (uint32_t i = 0; i < aStreams.Length(); ++i) { - MediaStream* stream = aStreams[i]; - - if (stream->mFinished) { - GraphTime endTime = StreamTimeToGraphTime(stream, stream->GetBufferEnd()); - if (endTime <= aTime) { - LOG(PR_LOG_DEBUG, ("MediaStream %p is blocked due to being finished", stream)); - // We'll block indefinitely - MarkStreamBlocking(stream); - *aEnd = aEndBlockingDecisions; - continue; - } else { - LOG(PR_LOG_DEBUG, ("MediaStream %p is finished, but not blocked yet (end at %f, with blocking at %f)", - stream, MediaTimeToSeconds(stream->GetBufferEnd()), - MediaTimeToSeconds(endTime))); - *aEnd = std::min(*aEnd, endTime); - } - } - - GraphTime end; - bool explicitBlock = stream->mExplicitBlockerCount.GetAt(aTime, &end) > 0; - *aEnd = std::min(*aEnd, end); - if (explicitBlock) { - LOG(PR_LOG_DEBUG, ("MediaStream %p is blocked due to explicit blocker", stream)); - MarkStreamBlocking(stream); - continue; - } - - bool underrun = WillUnderrun(stream, aTime, aEndBlockingDecisions, aEnd); - if (underrun) { - // We'll block indefinitely - MarkStreamBlocking(stream); - *aEnd = aEndBlockingDecisions; - continue; - } - } - NS_ASSERTION(*aEnd > aTime, "Failed to advance!"); - - for (uint32_t i = 0; i < aStreams.Length(); ++i) { - MediaStream* stream = aStreams[i]; - stream->mBlocked.SetAtAndAfter(aTime, stream->mBlockInThisPhase); - } -} - -void -MediaStreamGraphImpl::NotifyHasCurrentData(MediaStream* aStream) -{ - for (uint32_t j = 0; j < aStream->mListeners.Length(); ++j) { - MediaStreamListener* l = aStream->mListeners[j]; - l->NotifyHasCurrentData(this, - GraphTimeToStreamTime(aStream, mCurrentTime) < aStream->mBuffer.GetEnd()); - } -} - -void -MediaStreamGraphImpl::CreateOrDestroyAudioStreams(GraphTime aAudioOutputStartTime, - MediaStream* aStream) -{ - nsAutoTArray<bool,2> audioOutputStreamsFound; - for (uint32_t i = 0; i < aStream->mAudioOutputStreams.Length(); ++i) { - audioOutputStreamsFound.AppendElement(false); - } - - if (!aStream->mAudioOutputs.IsEmpty()) { - for (StreamBuffer::TrackIter tracks(aStream->GetStreamBuffer(), MediaSegment::AUDIO); - !tracks.IsEnded(); tracks.Next()) { - uint32_t i; - for (i = 0; i < audioOutputStreamsFound.Length(); ++i) { - if (aStream->mAudioOutputStreams[i].mTrackID == tracks->GetID()) { - break; - } - } - if (i < audioOutputStreamsFound.Length()) { - audioOutputStreamsFound[i] = true; - } else { - // No output stream created for this track yet. Check if it's time to - // create one. - GraphTime startTime = - StreamTimeToGraphTime(aStream, tracks->GetStartTimeRoundDown(), - INCLUDE_TRAILING_BLOCKED_INTERVAL); - if (startTime >= mStateComputedTime) { - // The stream wants to play audio, but nothing will play for the forseeable - // future, so don't create the stream. - continue; - } - - // XXX allocating a AudioStream could be slow so we're going to have to do - // something here ... preallocation, async allocation, multiplexing onto a single - // stream ... - MediaStream::AudioOutputStream* audioOutputStream = - aStream->mAudioOutputStreams.AppendElement(); - audioOutputStream->mAudioPlaybackStartTime = aAudioOutputStartTime; - audioOutputStream->mBlockedAudioTime = 0; - audioOutputStream->mStream = AudioStream::AllocateStream(); - // XXX for now, allocate stereo output. But we need to fix this to - // match the system's ideal channel configuration. - audioOutputStream->mStream->Init(2, tracks->GetRate(), AUDIO_CHANNEL_NORMAL); - audioOutputStream->mTrackID = tracks->GetID(); - } - } - } - - for (int32_t i = audioOutputStreamsFound.Length() - 1; i >= 0; --i) { - if (!audioOutputStreamsFound[i]) { - aStream->mAudioOutputStreams[i].mStream->Shutdown(); - aStream->mAudioOutputStreams.RemoveElementAt(i); - } - } -} - -void -MediaStreamGraphImpl::PlayAudio(MediaStream* aStream, - GraphTime aFrom, GraphTime aTo) -{ - if (aStream->mAudioOutputStreams.IsEmpty()) { - return; - } - - // When we're playing multiple copies of this stream at the same time, they're - // perfectly correlated so adding volumes is the right thing to do. - float volume = 0.0f; - for (uint32_t i = 0; i < aStream->mAudioOutputs.Length(); ++i) { - volume += aStream->mAudioOutputs[i].mVolume; - } - - for (uint32_t i = 0; i < aStream->mAudioOutputStreams.Length(); ++i) { - MediaStream::AudioOutputStream& audioOutput = aStream->mAudioOutputStreams[i]; - StreamBuffer::Track* track = aStream->mBuffer.FindTrack(audioOutput.mTrackID); - AudioSegment* audio = track->Get<AudioSegment>(); - - // We don't update aStream->mBufferStartTime here to account for - // time spent blocked. Instead, we'll update it in UpdateCurrentTime after the - // blocked period has completed. But we do need to make sure we play from the - // right offsets in the stream buffer, even if we've already written silence for - // some amount of blocked time after the current time. - GraphTime t = aFrom; - while (t < aTo) { - GraphTime end; - bool blocked = aStream->mBlocked.GetAt(t, &end); - end = std::min(end, aTo); - - AudioSegment output; - if (blocked) { - // Track total blocked time in aStream->mBlockedAudioTime so that - // the amount of silent samples we've inserted for blocking never gets - // more than one sample away from the ideal amount. - TrackTicks startTicks = - TimeToTicksRoundDown(track->GetRate(), audioOutput.mBlockedAudioTime); - audioOutput.mBlockedAudioTime += end - t; - TrackTicks endTicks = - TimeToTicksRoundDown(track->GetRate(), audioOutput.mBlockedAudioTime); - - output.InsertNullDataAtStart(endTicks - startTicks); - LOG(PR_LOG_DEBUG, ("MediaStream %p writing blocking-silence samples for %f to %f", - aStream, MediaTimeToSeconds(t), MediaTimeToSeconds(end))); - } else { - TrackTicks startTicks = - track->TimeToTicksRoundDown(GraphTimeToStreamTime(aStream, t)); - TrackTicks endTicks = - track->TimeToTicksRoundDown(GraphTimeToStreamTime(aStream, end)); - - // If startTicks is before the track start, then that part of 'audio' - // will just be silence, which is fine here. But if endTicks is after - // the track end, then 'audio' won't be long enough, so we'll need - // to explicitly play silence. - TrackTicks sliceEnd = std::min(endTicks, audio->GetDuration()); - if (sliceEnd > startTicks) { - output.AppendSlice(*audio, startTicks, sliceEnd); - } - // Play silence where the track has ended - output.AppendNullData(endTicks - sliceEnd); - NS_ASSERTION(endTicks == sliceEnd || track->IsEnded(), - "Ran out of data but track not ended?"); - output.ApplyVolume(volume); - LOG(PR_LOG_DEBUG, ("MediaStream %p writing samples for %f to %f (samples %lld to %lld)", - aStream, MediaTimeToSeconds(t), MediaTimeToSeconds(end), - startTicks, endTicks)); - } - output.WriteTo(audioOutput.mStream); - t = end; - } - } -} - -void -MediaStreamGraphImpl::PlayVideo(MediaStream* aStream) -{ - if (aStream->mVideoOutputs.IsEmpty()) - return; - - // Display the next frame a bit early. This is better than letting the current - // frame be displayed for too long. - GraphTime framePosition = mCurrentTime + MEDIA_GRAPH_TARGET_PERIOD_MS; - NS_ASSERTION(framePosition >= aStream->mBufferStartTime, "frame position before buffer?"); - StreamTime frameBufferTime = GraphTimeToStreamTime(aStream, framePosition); - - TrackTicks start; - const VideoFrame* frame = nullptr; - StreamBuffer::Track* track; - for (StreamBuffer::TrackIter tracks(aStream->GetStreamBuffer(), MediaSegment::VIDEO); - !tracks.IsEnded(); tracks.Next()) { - VideoSegment* segment = tracks->Get<VideoSegment>(); - TrackTicks thisStart; - const VideoFrame* thisFrame = - segment->GetFrameAt(tracks->TimeToTicksRoundDown(frameBufferTime), &thisStart); - if (thisFrame && thisFrame->GetImage()) { - start = thisStart; - frame = thisFrame; - track = tracks.get(); - } - } - if (!frame || *frame == aStream->mLastPlayedVideoFrame) - return; - - LOG(PR_LOG_DEBUG, ("MediaStream %p writing video frame %p (%dx%d)", - aStream, frame->GetImage(), frame->GetIntrinsicSize().width, - frame->GetIntrinsicSize().height)); - GraphTime startTime = StreamTimeToGraphTime(aStream, - track->TicksToTimeRoundDown(start), INCLUDE_TRAILING_BLOCKED_INTERVAL); - TimeStamp targetTime = mCurrentTimeStamp + - TimeDuration::FromMilliseconds(double(startTime - mCurrentTime)); - for (uint32_t i = 0; i < aStream->mVideoOutputs.Length(); ++i) { - VideoFrameContainer* output = aStream->mVideoOutputs[i]; - output->SetCurrentFrame(frame->GetIntrinsicSize(), frame->GetImage(), - targetTime); - nsCOMPtr<nsIRunnable> event = - NS_NewRunnableMethod(output, &VideoFrameContainer::Invalidate); - NS_DispatchToMainThread(event, NS_DISPATCH_NORMAL); - } - if (!aStream->mNotifiedFinished) { - aStream->mLastPlayedVideoFrame = *frame; - } -} - -void -MediaStreamGraphImpl::PrepareUpdatesToMainThreadState() -{ - mMonitor.AssertCurrentThreadOwns(); - - for (uint32_t i = 0; i < mStreams.Length(); ++i) { - MediaStream* stream = mStreams[i]; - StreamUpdate* update = mStreamUpdates.AppendElement(); - update->mGraphUpdateIndex = stream->mGraphUpdateIndices.GetAt(mCurrentTime); - update->mStream = stream; - update->mNextMainThreadCurrentTime = - GraphTimeToStreamTime(stream, mCurrentTime); - update->mNextMainThreadFinished = - stream->mFinished && - StreamTimeToGraphTime(stream, stream->GetBufferEnd()) <= mCurrentTime; - } - mUpdateRunnables.MoveElementsFrom(mPendingUpdateRunnables); - - EnsureStableStateEventPosted(); -} - -void -MediaStreamGraphImpl::EnsureImmediateWakeUpLocked(MonitorAutoLock& aLock) -{ - if (mWaitState == WAITSTATE_WAITING_FOR_NEXT_ITERATION || - mWaitState == WAITSTATE_WAITING_INDEFINITELY) { - mWaitState = WAITSTATE_WAKING_UP; - aLock.Notify(); - } -} - -void -MediaStreamGraphImpl::EnsureNextIteration() -{ - MonitorAutoLock lock(mMonitor); - EnsureNextIterationLocked(lock); -} - -void -MediaStreamGraphImpl::EnsureNextIterationLocked(MonitorAutoLock& aLock) -{ - if (mNeedAnotherIteration) - return; - mNeedAnotherIteration = true; - if (mWaitState == WAITSTATE_WAITING_INDEFINITELY) { - mWaitState = WAITSTATE_WAKING_UP; - aLock.Notify(); - } } -void -MediaStreamGraphImpl::RunThread() -{ - nsTArray<MessageBlock> messageQueue; - { - MonitorAutoLock lock(mMonitor); - messageQueue.SwapElements(mMessageQueue); - } - NS_ASSERTION(!messageQueue.IsEmpty(), - "Shouldn't have started a graph with empty message queue!"); - - for (;;) { - // Update mCurrentTime to the min of the playing audio times, or using the - // wall-clock time change if no audio is playing. - UpdateCurrentTime(); - - // Calculate independent action times for each batch of messages (each - // batch corresponding to an event loop task). This isolates the performance - // of different scripts to some extent. - for (uint32_t i = 0; i < messageQueue.Length(); ++i) { - mProcessingGraphUpdateIndex = messageQueue[i].mGraphUpdateIndex; - nsTArray<nsAutoPtr<ControlMessage> >& messages = messageQueue[i].mMessages; - - for (uint32_t j = 0; j < messages.Length(); ++j) { - messages[j]->Run(); - } - } - messageQueue.Clear(); - - UpdateStreamOrder(); - - int32_t writeAudioUpTo = AUDIO_TARGET_MS; - GraphTime endBlockingDecisions = - mCurrentTime + MillisecondsToMediaTime(writeAudioUpTo); - bool ensureNextIteration = false; - - // Grab pending stream input. - for (uint32_t i = 0; i < mStreams.Length(); ++i) { - SourceMediaStream* is = mStreams[i]->AsSourceStream(); - if (is) { - UpdateConsumptionState(is); - ExtractPendingInput(is, endBlockingDecisions, &ensureNextIteration); - } - } - - // Figure out which streams are blocked and when. - GraphTime prevComputedTime = mStateComputedTime; - RecomputeBlocking(endBlockingDecisions); - - // Play stream contents. - uint32_t audioStreamsActive = 0; - bool allBlockedForever = true; - // Figure out what each stream wants to do - for (uint32_t i = 0; i < mStreams.Length(); ++i) { - MediaStream* stream = mStreams[i]; - ProcessedMediaStream* ps = stream->AsProcessedStream(); - if (ps && !ps->mFinished) { - ps->ProduceOutput(prevComputedTime, mStateComputedTime); - NS_ASSERTION(stream->mBuffer.GetEnd() >= - GraphTimeToStreamTime(stream, mStateComputedTime), - "Stream did not produce enough data"); - } - NotifyHasCurrentData(stream); - CreateOrDestroyAudioStreams(prevComputedTime, stream); - PlayAudio(stream, prevComputedTime, mStateComputedTime); - audioStreamsActive += stream->mAudioOutputStreams.Length(); - PlayVideo(stream); - SourceMediaStream* is = stream->AsSourceStream(); - if (is) { - UpdateBufferSufficiencyState(is); - } - GraphTime end; - if (!stream->mBlocked.GetAt(mCurrentTime, &end) || end < GRAPH_TIME_MAX) { - allBlockedForever = false; - } - } - if (ensureNextIteration || !allBlockedForever || audioStreamsActive > 0) { - EnsureNextIteration(); - } - - // Send updates to the main thread and wait for the next control loop - // iteration. - { - // Not using MonitorAutoLock since we need to unlock in a way - // that doesn't match lexical scopes. - MonitorAutoLock lock(mMonitor); - PrepareUpdatesToMainThreadState(); - if (mForceShutDown || (IsEmpty() && mMessageQueue.IsEmpty())) { - // Enter shutdown mode. The stable-state handler will detect this - // and complete shutdown. Destroy any streams immediately. - LOG(PR_LOG_DEBUG, ("MediaStreamGraph %p waiting for main thread cleanup", this)); - // Commit to shutting down this graph object. - mLifecycleState = LIFECYCLE_WAITING_FOR_MAIN_THREAD_CLEANUP; - // No need to Destroy streams here. The main-thread owner of each - // stream is responsible for calling Destroy them. - return; - } - - PRIntervalTime timeout = PR_INTERVAL_NO_TIMEOUT; - TimeStamp now = TimeStamp::Now(); - if (mNeedAnotherIteration) { - int64_t timeoutMS = MEDIA_GRAPH_TARGET_PERIOD_MS - - int64_t((now - mCurrentTimeStamp).ToMilliseconds()); - // Make sure timeoutMS doesn't overflow 32 bits by waking up at - // least once a minute, if we need to wake up at all - timeoutMS = std::max<int64_t>(0, std::min<int64_t>(timeoutMS, 60*1000)); - timeout = PR_MillisecondsToInterval(uint32_t(timeoutMS)); - LOG(PR_LOG_DEBUG, ("Waiting for next iteration; at %f, timeout=%f", - (now - mInitialTimeStamp).ToSeconds(), timeoutMS/1000.0)); - mWaitState = WAITSTATE_WAITING_FOR_NEXT_ITERATION; - } else { - mWaitState = WAITSTATE_WAITING_INDEFINITELY; - } - if (timeout > 0) { - mMonitor.Wait(timeout); - LOG(PR_LOG_DEBUG, ("Resuming after timeout; at %f, elapsed=%f", - (TimeStamp::Now() - mInitialTimeStamp).ToSeconds(), - (TimeStamp::Now() - now).ToSeconds())); - } - mWaitState = WAITSTATE_RUNNING; - mNeedAnotherIteration = false; - messageQueue.SwapElements(mMessageQueue); - } - } -} - -void -MediaStreamGraphImpl::ApplyStreamUpdate(StreamUpdate* aUpdate) -{ - mMonitor.AssertCurrentThreadOwns(); - - MediaStream* stream = aUpdate->mStream; - if (!stream) - return; - stream->mMainThreadCurrentTime = aUpdate->mNextMainThreadCurrentTime; - stream->mMainThreadFinished = aUpdate->mNextMainThreadFinished; - - for (int32_t i = stream->mMainThreadListeners.Length() - 1; i >= 0; --i) { - stream->mMainThreadListeners[i]->NotifyMainThreadStateChanged(); - } -} - -void -MediaStreamGraphImpl::ShutdownThreads() -{ - NS_ASSERTION(NS_IsMainThread(), "Must be called on main thread"); - // mGraph's thread is not running so it's OK to do whatever here - LOG(PR_LOG_DEBUG, ("Stopping threads for MediaStreamGraph %p", this)); - - if (mThread) { - mThread->Shutdown(); - mThread = nullptr; - } -} - -void -MediaStreamGraphImpl::ForceShutDown() -{ - NS_ASSERTION(NS_IsMainThread(), "Must be called on main thread"); - LOG(PR_LOG_DEBUG, ("MediaStreamGraph %p ForceShutdown", this)); - { - MonitorAutoLock lock(mMonitor); - mForceShutDown = true; - EnsureImmediateWakeUpLocked(lock); - } -} - -namespace { - -class MediaStreamGraphThreadRunnable : public nsRunnable { -public: - NS_IMETHOD Run() - { - gGraph->RunThread(); - return NS_OK; - } -}; - -class MediaStreamGraphShutDownRunnable : public nsRunnable { -public: - MediaStreamGraphShutDownRunnable(MediaStreamGraphImpl* aGraph) : mGraph(aGraph) {} - NS_IMETHOD Run() - { - NS_ASSERTION(mGraph->mDetectedNotRunning, - "We should know the graph thread control loop isn't running!"); - // mGraph's thread is not running so it's OK to do whatever here - if (mGraph->IsEmpty()) { - // mGraph is no longer needed, so delete it. If the graph is not empty - // then we must be in a forced shutdown and some later AppendMessage will - // detect that the manager has been emptied, and delete it. - delete mGraph; - } else { - NS_ASSERTION(mGraph->mForceShutDown, "Not in forced shutdown?"); - mGraph->mLifecycleState = - MediaStreamGraphImpl::LIFECYCLE_WAITING_FOR_STREAM_DESTRUCTION; - } - return NS_OK; - } -private: - MediaStreamGraphImpl* mGraph; -}; - -class MediaStreamGraphStableStateRunnable : public nsRunnable { -public: - NS_IMETHOD Run() - { - if (gGraph) { - gGraph->RunInStableState(); - } - return NS_OK; - } -}; - -/* - * Control messages forwarded from main thread to graph manager thread - */ -class CreateMessage : public ControlMessage { -public: - CreateMessage(MediaStream* aStream) : ControlMessage(aStream) {} - virtual void Run() - { - mStream->GraphImpl()->AddStream(mStream); - mStream->Init(); - } -}; - -class MediaStreamGraphShutdownObserver MOZ_FINAL : public nsIObserver -{ -public: - NS_DECL_ISUPPORTS - NS_DECL_NSIOBSERVER -}; - -} - -void -MediaStreamGraphImpl::RunInStableState() -{ - NS_ASSERTION(NS_IsMainThread(), "Must be called on main thread"); - - nsTArray<nsCOMPtr<nsIRunnable> > runnables; - // When we're doing a forced shutdown, pending control messages may be - // run on the main thread via RunDuringShutdown. Those messages must - // run without the graph monitor being held. So, we collect them here. - nsTArray<nsAutoPtr<ControlMessage> > controlMessagesToRunDuringShutdown; - - { - MonitorAutoLock lock(mMonitor); - mPostedRunInStableStateEvent = false; - - runnables.SwapElements(mUpdateRunnables); - for (uint32_t i = 0; i < mStreamUpdates.Length(); ++i) { - StreamUpdate* update = &mStreamUpdates[i]; - if (update->mStream) { - ApplyStreamUpdate(update); - } - } - mStreamUpdates.Clear(); - - if (mLifecycleState == LIFECYCLE_WAITING_FOR_MAIN_THREAD_CLEANUP && mForceShutDown) { - // Defer calls to RunDuringShutdown() to happen while mMonitor is not held. - for (uint32_t i = 0; i < mMessageQueue.Length(); ++i) { - MessageBlock& mb = mMessageQueue[i]; - controlMessagesToRunDuringShutdown.MoveElementsFrom(mb.mMessages); - } - mMessageQueue.Clear(); - controlMessagesToRunDuringShutdown.MoveElementsFrom(mCurrentTaskMessageQueue); - // Stop MediaStreamGraph threads. Do not clear gGraph since - // we have outstanding DOM objects that may need it. - mLifecycleState = LIFECYCLE_WAITING_FOR_THREAD_SHUTDOWN; - nsCOMPtr<nsIRunnable> event = new MediaStreamGraphShutDownRunnable(this); - NS_DispatchToMainThread(event); - } - - if (mLifecycleState == LIFECYCLE_THREAD_NOT_STARTED) { - mLifecycleState = LIFECYCLE_RUNNING; - // Start the thread now. We couldn't start it earlier because - // the graph might exit immediately on finding it has no streams. The - // first message for a new graph must create a stream. - nsCOMPtr<nsIRunnable> event = new MediaStreamGraphThreadRunnable(); - NS_NewThread(getter_AddRefs(mThread), event); - } - - if (mCurrentTaskMessageQueue.IsEmpty()) { - if (mLifecycleState == LIFECYCLE_WAITING_FOR_MAIN_THREAD_CLEANUP && IsEmpty()) { - NS_ASSERTION(gGraph == this, "Not current graph??"); - // Complete shutdown. First, ensure that this graph is no longer used. - // A new graph graph will be created if one is needed. - LOG(PR_LOG_DEBUG, ("Disconnecting MediaStreamGraph %p", gGraph)); - gGraph = nullptr; - // Asynchronously clean up old graph. We don't want to do this - // synchronously because it spins the event loop waiting for threads - // to shut down, and we don't want to do that in a stable state handler. - mLifecycleState = LIFECYCLE_WAITING_FOR_THREAD_SHUTDOWN; - nsCOMPtr<nsIRunnable> event = new MediaStreamGraphShutDownRunnable(this); - NS_DispatchToMainThread(event); - } - } else { - if (mLifecycleState <= LIFECYCLE_WAITING_FOR_MAIN_THREAD_CLEANUP) { - MessageBlock* block = mMessageQueue.AppendElement(); - block->mMessages.SwapElements(mCurrentTaskMessageQueue); - block->mGraphUpdateIndex = mGraphUpdatesSent; - ++mGraphUpdatesSent; - EnsureNextIterationLocked(lock); - } - - if (mLifecycleState == LIFECYCLE_WAITING_FOR_MAIN_THREAD_CLEANUP) { - mLifecycleState = LIFECYCLE_RUNNING; - // Revive the MediaStreamGraph since we have more messages going to it. - // Note that we need to put messages into its queue before reviving it, - // or it might exit immediately. - nsCOMPtr<nsIRunnable> event = new MediaStreamGraphThreadRunnable(); - mThread->Dispatch(event, 0); - } - } - - mDetectedNotRunning = mLifecycleState > LIFECYCLE_RUNNING; - } - - // Make sure we get a new current time in the next event loop task - mPostedRunInStableState = false; - - for (uint32_t i = 0; i < runnables.Length(); ++i) { - runnables[i]->Run(); - } - for (uint32_t i = 0; i < controlMessagesToRunDuringShutdown.Length(); ++i) { - controlMessagesToRunDuringShutdown[i]->RunDuringShutdown(); - } -} - -static NS_DEFINE_CID(kAppShellCID, NS_APPSHELL_CID); - -void -MediaStreamGraphImpl::EnsureRunInStableState() -{ - NS_ASSERTION(NS_IsMainThread(), "main thread only"); - - if (mPostedRunInStableState) - return; - mPostedRunInStableState = true; - nsCOMPtr<nsIRunnable> event = new MediaStreamGraphStableStateRunnable(); - nsCOMPtr<nsIAppShell> appShell = do_GetService(kAppShellCID); - if (appShell) { - appShell->RunInStableState(event); - } else { - NS_ERROR("Appshell already destroyed?"); - } -} - -void -MediaStreamGraphImpl::EnsureStableStateEventPosted() -{ - mMonitor.AssertCurrentThreadOwns(); - - if (mPostedRunInStableStateEvent) - return; - mPostedRunInStableStateEvent = true; - nsCOMPtr<nsIRunnable> event = new MediaStreamGraphStableStateRunnable(); - NS_DispatchToMainThread(event); -} - -void -MediaStreamGraphImpl::AppendMessage(ControlMessage* aMessage) -{ - NS_ASSERTION(NS_IsMainThread(), "main thread only"); - NS_ASSERTION(!aMessage->GetStream() || - !aMessage->GetStream()->IsDestroyed(), - "Stream already destroyed"); - - if (mDetectedNotRunning && - mLifecycleState > LIFECYCLE_WAITING_FOR_MAIN_THREAD_CLEANUP) { - // The graph control loop is not running and main thread cleanup has - // happened. From now on we can't append messages to mCurrentTaskMessageQueue, - // because that will never be processed again, so just RunDuringShutdown - // this message. - // This should only happen during forced shutdown. - aMessage->RunDuringShutdown(); - delete aMessage; - if (IsEmpty()) { - NS_ASSERTION(gGraph == this, "Switched managers during forced shutdown?"); - gGraph = nullptr; - delete this; - } - return; - } - - mCurrentTaskMessageQueue.AppendElement(aMessage); - EnsureRunInStableState(); -} - -void -MediaStream::Init() -{ - MediaStreamGraphImpl* graph = GraphImpl(); - mBlocked.SetAtAndAfter(graph->mCurrentTime, true); - mExplicitBlockerCount.SetAtAndAfter(graph->mCurrentTime, true); - mExplicitBlockerCount.SetAtAndAfter(graph->mStateComputedTime, false); -} - -MediaStreamGraphImpl* -MediaStream::GraphImpl() -{ - return gGraph; -} - -MediaStreamGraph* -MediaStream::Graph() -{ - return gGraph; -} - -StreamTime -MediaStream::GraphTimeToStreamTime(GraphTime aTime) -{ - return GraphImpl()->GraphTimeToStreamTime(this, aTime); -} - -void -MediaStream::FinishOnGraphThread() -{ - GraphImpl()->FinishStream(this); -} - -void -MediaStream::RemoveAllListenersImpl() -{ - for (int32_t i = mListeners.Length() - 1; i >= 0; --i) { - nsRefPtr<MediaStreamListener> listener = mListeners[i].forget(); - listener->NotifyRemoved(GraphImpl()); - } - mListeners.Clear(); -} - -void -MediaStream::DestroyImpl() -{ - RemoveAllListenersImpl(); - - for (int32_t i = mConsumers.Length() - 1; i >= 0; --i) { - mConsumers[i]->Disconnect(); - } - for (uint32_t i = 0; i < mAudioOutputStreams.Length(); ++i) { - mAudioOutputStreams[i].mStream->Shutdown(); - } - mAudioOutputStreams.Clear(); -} - -void -MediaStream::Destroy() -{ - // Keep this stream alive until we leave this method - nsRefPtr<MediaStream> kungFuDeathGrip = this; - - class Message : public ControlMessage { - public: - Message(MediaStream* aStream) : ControlMessage(aStream) {} - virtual void Run() - { - mStream->DestroyImpl(); - mStream->GraphImpl()->RemoveStream(mStream); - } - virtual void RunDuringShutdown() - { Run(); } - }; - mWrapper = nullptr; - GraphImpl()->AppendMessage(new Message(this)); - // Message::RunDuringShutdown may have removed this stream from the graph, - // but our kungFuDeathGrip above will have kept this stream alive if - // necessary. - mMainThreadDestroyed = true; -} - -void -MediaStream::AddAudioOutput(void* aKey) -{ - class Message : public ControlMessage { - public: - Message(MediaStream* aStream, void* aKey) : ControlMessage(aStream), mKey(aKey) {} - virtual void Run() - { - mStream->AddAudioOutputImpl(mKey); - } - void* mKey; - }; - GraphImpl()->AppendMessage(new Message(this, aKey)); -} - -void -MediaStream::SetAudioOutputVolumeImpl(void* aKey, float aVolume) -{ - for (uint32_t i = 0; i < mAudioOutputs.Length(); ++i) { - if (mAudioOutputs[i].mKey == aKey) { - mAudioOutputs[i].mVolume = aVolume; - return; - } - } - NS_ERROR("Audio output key not found"); -} - -void -MediaStream::SetAudioOutputVolume(void* aKey, float aVolume) -{ - class Message : public ControlMessage { - public: - Message(MediaStream* aStream, void* aKey, float aVolume) : - ControlMessage(aStream), mKey(aKey), mVolume(aVolume) {} - virtual void Run() - { - mStream->SetAudioOutputVolumeImpl(mKey, mVolume); - } - void* mKey; - float mVolume; - }; - GraphImpl()->AppendMessage(new Message(this, aKey, aVolume)); -} - -void -MediaStream::RemoveAudioOutputImpl(void* aKey) -{ - for (uint32_t i = 0; i < mAudioOutputs.Length(); ++i) { - if (mAudioOutputs[i].mKey == aKey) { - mAudioOutputs.RemoveElementAt(i); - return; - } - } - NS_ERROR("Audio output key not found"); -} - -void -MediaStream::RemoveAudioOutput(void* aKey) -{ - class Message : public ControlMessage { - public: - Message(MediaStream* aStream, void* aKey) : - ControlMessage(aStream), mKey(aKey) {} - virtual void Run() - { - mStream->RemoveAudioOutputImpl(mKey); - } - void* mKey; - }; - GraphImpl()->AppendMessage(new Message(this, aKey)); -} - -void -MediaStream::AddVideoOutput(VideoFrameContainer* aContainer) -{ - class Message : public ControlMessage { - public: - Message(MediaStream* aStream, VideoFrameContainer* aContainer) : - ControlMessage(aStream), mContainer(aContainer) {} - virtual void Run() - { - mStream->AddVideoOutputImpl(mContainer.forget()); - } - nsRefPtr<VideoFrameContainer> mContainer; - }; - GraphImpl()->AppendMessage(new Message(this, aContainer)); -} - -void -MediaStream::RemoveVideoOutput(VideoFrameContainer* aContainer) -{ - class Message : public ControlMessage { - public: - Message(MediaStream* aStream, VideoFrameContainer* aContainer) : - ControlMessage(aStream), mContainer(aContainer) {} - virtual void Run() - { - mStream->RemoveVideoOutputImpl(mContainer); - } - nsRefPtr<VideoFrameContainer> mContainer; - }; - GraphImpl()->AppendMessage(new Message(this, aContainer)); -} - -void -MediaStream::ChangeExplicitBlockerCount(int32_t aDelta) -{ - class Message : public ControlMessage { - public: - Message(MediaStream* aStream, int32_t aDelta) : - ControlMessage(aStream), mDelta(aDelta) {} - virtual void Run() - { - mStream->ChangeExplicitBlockerCountImpl( - mStream->GraphImpl()->mStateComputedTime, mDelta); - } - int32_t mDelta; - }; - GraphImpl()->AppendMessage(new Message(this, aDelta)); -} - -void -MediaStream::AddListenerImpl(already_AddRefed<MediaStreamListener> aListener) -{ - MediaStreamListener* listener = *mListeners.AppendElement() = aListener; - listener->NotifyBlockingChanged(GraphImpl(), - mNotifiedBlocked ? MediaStreamListener::BLOCKED : MediaStreamListener::UNBLOCKED); - if (mNotifiedFinished) { - listener->NotifyFinished(GraphImpl()); - } -} - -void -MediaStream::AddListener(MediaStreamListener* aListener) -{ - class Message : public ControlMessage { - public: - Message(MediaStream* aStream, MediaStreamListener* aListener) : - ControlMessage(aStream), mListener(aListener) {} - virtual void Run() - { - mStream->AddListenerImpl(mListener.forget()); - } - nsRefPtr<MediaStreamListener> mListener; - }; - GraphImpl()->AppendMessage(new Message(this, aListener)); -} - -void -MediaStream::RemoveListenerImpl(MediaStreamListener* aListener) -{ - // wouldn't need this if we could do it in the opposite order - nsRefPtr<MediaStreamListener> listener(aListener); - mListeners.RemoveElement(aListener); - listener->NotifyRemoved(GraphImpl()); -} - -void -MediaStream::RemoveListener(MediaStreamListener* aListener) -{ - class Message : public ControlMessage { - public: - Message(MediaStream* aStream, MediaStreamListener* aListener) : - ControlMessage(aStream), mListener(aListener) {} - virtual void Run() - { - mStream->RemoveListenerImpl(mListener); - } - nsRefPtr<MediaStreamListener> mListener; - }; - GraphImpl()->AppendMessage(new Message(this, aListener)); -} - -void -SourceMediaStream::DestroyImpl() -{ - { - MutexAutoLock lock(mMutex); - mDestroyed = true; - } - MediaStream::DestroyImpl(); -} - -void -SourceMediaStream::SetPullEnabled(bool aEnabled) -{ - MutexAutoLock lock(mMutex); - mPullEnabled = aEnabled; - if (mPullEnabled && !mDestroyed) { - GraphImpl()->EnsureNextIteration(); - } -} - -void -SourceMediaStream::AddTrack(TrackID aID, TrackRate aRate, TrackTicks aStart, - MediaSegment* aSegment) -{ - MutexAutoLock lock(mMutex); - TrackData* data = mUpdateTracks.AppendElement(); - data->mID = aID; - data->mRate = aRate; - data->mStart = aStart; - data->mCommands = TRACK_CREATE; - data->mData = aSegment; - data->mHaveEnough = false; - if (!mDestroyed) { - GraphImpl()->EnsureNextIteration(); - } -} - -void -SourceMediaStream::AppendToTrack(TrackID aID, MediaSegment* aSegment) -{ - MutexAutoLock lock(mMutex); - // ::EndAllTrackAndFinished() can end these before the sources notice - if (!mFinished) { - TrackData *track = FindDataForTrack(aID); - if (track) { - track->mData->AppendFrom(aSegment); - } else { - NS_ERROR("Append to non-existent track!"); - } - } - if (!mDestroyed) { - GraphImpl()->EnsureNextIteration(); - } -} - -bool -SourceMediaStream::HaveEnoughBuffered(TrackID aID) -{ - MutexAutoLock lock(mMutex); - TrackData *track = FindDataForTrack(aID); - if (track) { - return track->mHaveEnough; - } - NS_ERROR("No track in HaveEnoughBuffered!"); - return true; -} - -void -SourceMediaStream::DispatchWhenNotEnoughBuffered(TrackID aID, - nsIThread* aSignalThread, nsIRunnable* aSignalRunnable) -{ - MutexAutoLock lock(mMutex); - TrackData* data = FindDataForTrack(aID); - if (!data) { - NS_ERROR("No track in DispatchWhenNotEnoughBuffered"); - return; - } - - if (data->mHaveEnough) { - data->mDispatchWhenNotEnough.AppendElement()->Init(aSignalThread, aSignalRunnable); - } else { - aSignalThread->Dispatch(aSignalRunnable, 0); - } -} - -void -SourceMediaStream::EndTrack(TrackID aID) -{ - MutexAutoLock lock(mMutex); - // ::EndAllTrackAndFinished() can end these before the sources call this - if (!mFinished) { - TrackData *track = FindDataForTrack(aID); - if (track) { - track->mCommands |= TRACK_END; - } else { - NS_ERROR("End of non-existant track"); - } - } - if (!mDestroyed) { - GraphImpl()->EnsureNextIteration(); - } -} - -void -SourceMediaStream::AdvanceKnownTracksTime(StreamTime aKnownTime) -{ - MutexAutoLock lock(mMutex); - mUpdateKnownTracksTime = aKnownTime; - if (!mDestroyed) { - GraphImpl()->EnsureNextIteration(); - } -} - -void -SourceMediaStream::FinishWithLockHeld() -{ - mUpdateFinished = true; - if (!mDestroyed) { - GraphImpl()->EnsureNextIteration(); - } -} - -void -SourceMediaStream::EndAllTrackAndFinish() -{ - { - MutexAutoLock lock(mMutex); - for (uint32_t i = 0; i < mUpdateTracks.Length(); ++i) { - SourceMediaStream::TrackData* data = &mUpdateTracks[i]; - data->mCommands |= TRACK_END; - } - } - FinishWithLockHeld(); - // we will call NotifyFinished() to let GetUserMedia know -} - -void -MediaInputPort::Init() -{ - LOG(PR_LOG_DEBUG, ("Adding MediaInputPort %p (from %p to %p) to the graph", - this, mSource, mDest)); - mSource->AddConsumer(this); - mDest->AddInput(this); - // mPortCount decremented via MediaInputPort::Destroy's message - ++mDest->GraphImpl()->mPortCount; -} - -void -MediaInputPort::Disconnect() -{ - NS_ASSERTION(!mSource == !mDest, - "mSource must either both be null or both non-null"); - if (!mSource) - return; - - mSource->RemoveConsumer(this); - mSource = nullptr; - mDest->RemoveInput(this); - mDest = nullptr; -} - -MediaInputPort::InputInterval -MediaInputPort::GetNextInputInterval(GraphTime aTime) -{ - InputInterval result = { GRAPH_TIME_MAX, GRAPH_TIME_MAX, false }; - GraphTime t = aTime; - GraphTime end; - for (;;) { - if (!mDest->mBlocked.GetAt(t, &end)) - break; - if (end == GRAPH_TIME_MAX) - return result; - t = end; - } - result.mStart = t; - GraphTime sourceEnd; - result.mInputIsBlocked = mSource->mBlocked.GetAt(t, &sourceEnd); - result.mEnd = std::min(end, sourceEnd); - return result; -} - -void -MediaInputPort::Destroy() -{ - class Message : public ControlMessage { - public: - Message(MediaInputPort* aPort) - : ControlMessage(nullptr), mPort(aPort) {} - virtual void Run() - { - mPort->Disconnect(); - --mPort->GraphImpl()->mPortCount; - NS_RELEASE(mPort); - } - virtual void RunDuringShutdown() - { - Run(); - } - // This does not need to be strongly referenced; the graph is holding - // a strong reference to the port, which we will remove. This will be the - // last message for the port. - MediaInputPort* mPort; - }; - GraphImpl()->AppendMessage(new Message(this)); -} - -MediaStreamGraphImpl* -MediaInputPort::GraphImpl() -{ - return gGraph; -} - -MediaStreamGraph* -MediaInputPort::Graph() -{ - return gGraph; -} - -MediaInputPort* -ProcessedMediaStream::AllocateInputPort(MediaStream* aStream, uint32_t aFlags) -{ - class Message : public ControlMessage { - public: - Message(MediaInputPort* aPort) - : ControlMessage(aPort->GetDestination()), - mPort(aPort) {} - virtual void Run() - { - mPort->Init(); - } - MediaInputPort* mPort; - }; - MediaInputPort* port = new MediaInputPort(aStream, this, aFlags); - NS_ADDREF(port); - GraphImpl()->AppendMessage(new Message(port)); - return port; -} - -void -ProcessedMediaStream::Finish() -{ - class Message : public ControlMessage { - public: - Message(ProcessedMediaStream* aStream) - : ControlMessage(aStream) {} - virtual void Run() - { - mStream->GraphImpl()->FinishStream(mStream); - } - }; - GraphImpl()->AppendMessage(new Message(this)); -} - -void -ProcessedMediaStream::SetAutofinish(bool aAutofinish) -{ - class Message : public ControlMessage { - public: - Message(ProcessedMediaStream* aStream, bool aAutofinish) - : ControlMessage(aStream), mAutofinish(aAutofinish) {} - virtual void Run() - { - mStream->AsProcessedStream()->SetAutofinishImpl(mAutofinish); - } - bool mAutofinish; - }; - GraphImpl()->AppendMessage(new Message(this, aAutofinish)); -} - -void -ProcessedMediaStream::DestroyImpl() -{ - for (int32_t i = mInputs.Length() - 1; i >= 0; --i) { - mInputs[i]->Disconnect(); - } - MediaStream::DestroyImpl(); -} - -/** - * We make the initial mCurrentTime nonzero so that zero times can have - * special meaning if necessary. - */ -static const int32_t INITIAL_CURRENT_TIME = 1; - -MediaStreamGraphImpl::MediaStreamGraphImpl() - : mCurrentTime(INITIAL_CURRENT_TIME) - , mStateComputedTime(INITIAL_CURRENT_TIME) - , mProcessingGraphUpdateIndex(0) - , mPortCount(0) - , mMonitor("MediaStreamGraphImpl") - , mLifecycleState(LIFECYCLE_THREAD_NOT_STARTED) - , mWaitState(WAITSTATE_RUNNING) - , mNeedAnotherIteration(false) - , mForceShutDown(false) - , mPostedRunInStableStateEvent(false) - , mDetectedNotRunning(false) - , mPostedRunInStableState(false) -{ -#ifdef PR_LOGGING - if (!gMediaStreamGraphLog) { - gMediaStreamGraphLog = PR_NewLogModule("MediaStreamGraph"); - } -#endif - - mCurrentTimeStamp = mInitialTimeStamp = TimeStamp::Now(); -} - -NS_IMPL_ISUPPORTS1(MediaStreamGraphShutdownObserver, nsIObserver) - -static bool gShutdownObserverRegistered = false; - -NS_IMETHODIMP -MediaStreamGraphShutdownObserver::Observe(nsISupports *aSubject, - const char *aTopic, - const PRUnichar *aData) -{ - if (strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0) { - if (gGraph) { - gGraph->ForceShutDown(); - } - nsContentUtils::UnregisterShutdownObserver(this); - gShutdownObserverRegistered = false; - } - return NS_OK; -} - -MediaStreamGraph* -MediaStreamGraph::GetInstance() -{ - NS_ASSERTION(NS_IsMainThread(), "Main thread only"); - - if (!gGraph) { - if (!gShutdownObserverRegistered) { - gShutdownObserverRegistered = true; - nsContentUtils::RegisterShutdownObserver(new MediaStreamGraphShutdownObserver()); - } - - gGraph = new MediaStreamGraphImpl(); - LOG(PR_LOG_DEBUG, ("Starting up MediaStreamGraph %p", gGraph)); - } - - return gGraph; -} - -SourceMediaStream* -MediaStreamGraph::CreateSourceStream(nsDOMMediaStream* aWrapper) -{ - SourceMediaStream* stream = new SourceMediaStream(aWrapper); - NS_ADDREF(stream); - static_cast<MediaStreamGraphImpl*>(this)->AppendMessage(new CreateMessage(stream)); - return stream; -} - -ProcessedMediaStream* -MediaStreamGraph::CreateTrackUnionStream(nsDOMMediaStream* aWrapper) -{ - TrackUnionStream* stream = new TrackUnionStream(aWrapper); - NS_ADDREF(stream); - static_cast<MediaStreamGraphImpl*>(this)->AppendMessage(new CreateMessage(stream)); - return stream; -} - -} +#endif /* MEDIASTREAMGRAPHIMPL_H_ */
--- a/content/media/StreamBuffer.h +++ b/content/media/StreamBuffer.h @@ -30,28 +30,28 @@ const TrackRate TRACK_RATE_MAX = 1 << ME /** * Unique ID for track within a StreamBuffer. Tracks from different * StreamBuffers may have the same ID; this matters when appending StreamBuffers, * since tracks with the same ID are matched. Only IDs greater than 0 are allowed. */ typedef int32_t TrackID; const TrackID TRACK_NONE = 0; -inline TrackTicks TimeToTicksRoundUp(TrackRate aRate, StreamTime aMicroseconds) +inline TrackTicks TimeToTicksRoundUp(TrackRate aRate, StreamTime aTime) { NS_ASSERTION(0 < aRate && aRate <= TRACK_RATE_MAX, "Bad rate"); - NS_ASSERTION(0 <= aMicroseconds && aMicroseconds <= STREAM_TIME_MAX, "Bad microseconds"); - return (aMicroseconds*aRate + (1 << MEDIA_TIME_FRAC_BITS) - 1) >> MEDIA_TIME_FRAC_BITS; + NS_ASSERTION(0 <= aTime && aTime <= STREAM_TIME_MAX, "Bad time"); + return (aTime*aRate + (1 << MEDIA_TIME_FRAC_BITS) - 1) >> MEDIA_TIME_FRAC_BITS; } -inline TrackTicks TimeToTicksRoundDown(TrackRate aRate, StreamTime aMicroseconds) +inline TrackTicks TimeToTicksRoundDown(TrackRate aRate, StreamTime aTime) { NS_ASSERTION(0 < aRate && aRate <= TRACK_RATE_MAX, "Bad rate"); - NS_ASSERTION(0 <= aMicroseconds && aMicroseconds <= STREAM_TIME_MAX, "Bad microseconds"); - return (aMicroseconds*aRate) >> MEDIA_TIME_FRAC_BITS; + NS_ASSERTION(0 <= aTime && aTime <= STREAM_TIME_MAX, "Bad time"); + return (aTime*aRate) >> MEDIA_TIME_FRAC_BITS; } inline StreamTime TicksToTimeRoundUp(TrackRate aRate, TrackTicks aTicks) { NS_ASSERTION(0 < aRate && aRate <= TRACK_RATE_MAX, "Bad rate"); NS_ASSERTION(0 <= aTicks && aTicks <= TRACK_TICKS_MAX, "Bad samples"); return ((aTicks << MEDIA_TIME_FRAC_BITS) + aRate - 1)/aRate; }
--- a/content/media/webaudio/AudioBuffer.cpp +++ b/content/media/webaudio/AudioBuffer.cpp @@ -5,48 +5,49 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "AudioBuffer.h" #include "mozilla/dom/AudioBufferBinding.h" #include "nsContentUtils.h" #include "AudioContext.h" #include "jsfriendapi.h" #include "mozilla/ErrorResult.h" +#include "AudioSegment.h" namespace mozilla { namespace dom { NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(AudioBuffer) NS_IMPL_CYCLE_COLLECTION_UNLINK(mContext) - NS_IMPL_CYCLE_COLLECTION_UNLINK(mChannels) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mJSChannels) NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER tmp->ClearJSChannels(); NS_IMPL_CYCLE_COLLECTION_UNLINK_END NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(AudioBuffer) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mContext) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(AudioBuffer) NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER - for (uint32_t i = 0; i < tmp->mChannels.Length(); ++i) { - NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mChannels[i]) + for (uint32_t i = 0; i < tmp->mJSChannels.Length(); ++i) { + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mJSChannels[i]) } NS_IMPL_CYCLE_COLLECTION_TRACE_END NS_IMPL_CYCLE_COLLECTING_ADDREF(AudioBuffer) NS_IMPL_CYCLE_COLLECTING_RELEASE(AudioBuffer) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AudioBuffer) NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY NS_INTERFACE_MAP_ENTRY(nsISupports) NS_INTERFACE_MAP_END AudioBuffer::AudioBuffer(AudioContext* aContext, uint32_t aLength, - uint32_t aSampleRate) + float aSampleRate) : mContext(aContext), mLength(aLength), mSampleRate(aSampleRate) { SetIsDOMBinding(); NS_HOLD_JS_OBJECTS(this, AudioBuffer); } @@ -54,62 +55,154 @@ AudioBuffer::AudioBuffer(AudioContext* a AudioBuffer::~AudioBuffer() { ClearJSChannels(); } void AudioBuffer::ClearJSChannels() { - mChannels.Clear(); + mJSChannels.Clear(); NS_DROP_JS_OBJECTS(this, AudioBuffer); } bool AudioBuffer::InitializeBuffers(uint32_t aNumberOfChannels, JSContext* aJSContext) { - if (!mChannels.SetCapacity(aNumberOfChannels)) { + if (!mJSChannels.SetCapacity(aNumberOfChannels)) { return false; } for (uint32_t i = 0; i < aNumberOfChannels; ++i) { JSObject* array = JS_NewFloat32Array(aJSContext, mLength); if (!array) { return false; } - mChannels.AppendElement(array); + mJSChannels.AppendElement(array); } return true; } JSObject* AudioBuffer::WrapObject(JSContext* aCx, JSObject* aScope, bool* aTriedToWrap) { return AudioBufferBinding::Wrap(aCx, aScope, this, aTriedToWrap); } +void +AudioBuffer::RestoreJSChannelData(JSContext* aJSContext) +{ + if (mSharedChannels) { + for (uint32_t i = 0; i < mJSChannels.Length(); ++i) { + const float* data = mSharedChannels->GetData(i); + // The following code first zeroes the array and then copies our data + // into it. We could avoid this with additional JS APIs to construct + // an array (or ArrayBuffer) containing initial data. + JSObject* array = JS_NewFloat32Array(aJSContext, mLength); + memcpy(JS_GetFloat32ArrayData(array), data, sizeof(float)*mLength); + mJSChannels[i] = array; + } + + mSharedChannels = nullptr; + mResampledChannels = nullptr; + } +} + JSObject* AudioBuffer::GetChannelData(JSContext* aJSContext, uint32_t aChannel, - ErrorResult& aRv) const + ErrorResult& aRv) { - if (aChannel >= mChannels.Length()) { + if (aChannel >= NumberOfChannels()) { aRv.Throw(NS_ERROR_DOM_SYNTAX_ERR); return nullptr; } - return GetChannelData(aChannel); + + RestoreJSChannelData(aJSContext); + + return mJSChannels[aChannel]; } void AudioBuffer::SetChannelDataFromArrayBufferContents(JSContext* aJSContext, uint32_t aChannel, void* aContents) { - MOZ_ASSERT(aChannel < mChannels.Length()); + RestoreJSChannelData(aJSContext); + + MOZ_ASSERT(aChannel < NumberOfChannels()); JSObject* arrayBuffer = JS_NewArrayBufferWithContents(aJSContext, aContents); - mChannels[aChannel] = JS_NewFloat32ArrayWithBuffer(aJSContext, arrayBuffer, - 0, -1); - MOZ_ASSERT(mLength == JS_GetTypedArrayLength(mChannels[aChannel])); + mJSChannels[aChannel] = JS_NewFloat32ArrayWithBuffer(aJSContext, arrayBuffer, + 0, -1); + MOZ_ASSERT(mLength == JS_GetTypedArrayLength(mJSChannels[aChannel])); +} + +static already_AddRefed<ThreadSharedFloatArrayBufferList> +StealJSArrayDataIntoThreadSharedFloatArrayBufferList(JSContext* aJSContext, + const nsTArray<JSObject*>& aJSArrays) +{ + nsRefPtr<ThreadSharedFloatArrayBufferList> result = + new ThreadSharedFloatArrayBufferList(aJSArrays.Length()); + for (uint32_t i = 0; i < aJSArrays.Length(); ++i) { + JSObject* arrayBuffer = JS_GetArrayBufferViewBuffer(aJSArrays[i]); + void* dataToFree = nullptr; + uint8_t* stolenData = nullptr; + if (arrayBuffer && + JS_StealArrayBufferContents(aJSContext, arrayBuffer, &dataToFree, + &stolenData)) { + result->SetData(i, dataToFree, reinterpret_cast<float*>(stolenData)); + } else { + result->Clear(); + return result.forget(); + } + } + return result.forget(); } +ThreadSharedFloatArrayBufferList* +AudioBuffer::GetThreadSharedChannelsForRate(JSContext* aJSContext, uint32_t aRate, + uint32_t* aLength) +{ + if (mResampledChannels && mResampledChannelsRate == aRate) { + // return cached data + *aLength = mResampledChannelsLength; + return mResampledChannels; + } + + if (!mSharedChannels) { + // Steal JS data + mSharedChannels = + StealJSArrayDataIntoThreadSharedFloatArrayBufferList(aJSContext, mJSChannels); + } + + if (mSampleRate == aRate) { + *aLength = mLength; + return mSharedChannels; + } + + mResampledChannels = new ThreadSharedFloatArrayBufferList(NumberOfChannels()); + + double newLengthD = ceil(Duration()*aRate); + uint32_t newLength = uint32_t(newLengthD); + *aLength = newLength; + double size = sizeof(float)*NumberOfChannels()*newLengthD; + if (size != uint32_t(size)) { + return mResampledChannels; + } + float* outputData = static_cast<float*>(malloc(uint32_t(size))); + if (!outputData) { + return mResampledChannels; + } + + for (uint32_t i = 0; i < NumberOfChannels(); ++i) { + NS_ERROR("Resampling not supported yet"); + // const float* inputData = mSharedChannels->GetData(i); + // Resample(inputData, mLength, mSampleRate, outputData, newLength, aRate); + mResampledChannels->SetData(i, i == 0 ? outputData : nullptr, outputData); + outputData += newLength; + } + mResampledChannelsRate = aRate; + mResampledChannelsLength = newLength; + return mResampledChannels; +} + } } -
--- a/content/media/webaudio/AudioBuffer.h +++ b/content/media/webaudio/AudioBuffer.h @@ -9,33 +9,40 @@ #include "nsWrapperCache.h" #include "nsCycleCollectionParticipant.h" #include "mozilla/Attributes.h" #include "EnableWebAudioCheck.h" #include "nsAutoPtr.h" #include "nsTArray.h" #include "AudioContext.h" +#include "AudioSegment.h" +#include "AudioNodeEngine.h" struct JSContext; class JSObject; namespace mozilla { class ErrorResult; namespace dom { +/** + * An AudioBuffer keeps its data either in the mJSChannels objects, which + * are Float32Arrays, or in mSharedChannels if the mJSChannels objects have + * been neutered. + */ class AudioBuffer MOZ_FINAL : public nsISupports, public nsWrapperCache, public EnableWebAudioCheck { public: AudioBuffer(AudioContext* aContext, uint32_t aLength, - uint32_t aSampleRate); + float aSampleRate); ~AudioBuffer(); // This function needs to be called in order to allocate // all of the channels. It is fallible! bool InitializeBuffers(uint32_t aNumberOfChannels, JSContext* aJSContext); NS_DECL_CYCLE_COLLECTING_ISUPPORTS @@ -49,50 +56,76 @@ public: virtual JSObject* WrapObject(JSContext* aCx, JSObject* aScope, bool* aTriedToWrap); float SampleRate() const { return mSampleRate; } - uint32_t Length() const + int32_t Length() const { return mLength; } double Duration() const { return mLength / static_cast<double> (mSampleRate); } uint32_t NumberOfChannels() const { - return mChannels.Length(); + return mJSChannels.Length(); } + /** + * If mSharedChannels is non-null, copies its contents to + * new Float32Arrays in mJSChannels. Returns a Float32Array. + */ JSObject* GetChannelData(JSContext* aJSContext, uint32_t aChannel, - ErrorResult& aRv) const; + ErrorResult& aRv); + JSObject* GetChannelData(uint32_t aChannel) const { // Doesn't perform bounds checking - MOZ_ASSERT(aChannel < mChannels.Length()); - return mChannels[aChannel]; + MOZ_ASSERT(aChannel < mJSChannels.Length()); + return mJSChannels[aChannel]; } + /** + * Returns a ThreadSharedFloatArrayBufferList containing the sample data + * at aRate. Sets *aLength to the number of samples per channel. + */ + ThreadSharedFloatArrayBufferList* GetThreadSharedChannelsForRate(JSContext* aContext, + uint32_t aRate, + uint32_t* aLength); + // aContents should either come from JS_AllocateArrayBufferContents or // JS_StealArrayBufferContents. void SetChannelDataFromArrayBufferContents(JSContext* aJSContext, uint32_t aChannel, void* aContents); -private: +protected: + void RestoreJSChannelData(JSContext* aJSContext); void ClearJSChannels(); nsRefPtr<AudioContext> mContext; - FallibleTArray<JSObject*> mChannels; + // Float32Arrays + nsAutoTArray<JSObject*,2> mJSChannels; + + // mSharedChannels aggregates the data from mJSChannels. This is non-null + // if and only if the mJSChannels are neutered. + nsRefPtr<ThreadSharedFloatArrayBufferList> mSharedChannels; + + // One-element cache of resampled data. Can be non-null only if mSharedChannels + // is non-null. + nsRefPtr<ThreadSharedFloatArrayBufferList> mResampledChannels; + uint32_t mResampledChannelsRate; + uint32_t mResampledChannelsLength; + uint32_t mLength; float mSampleRate; }; } } #endif
--- a/content/media/webaudio/AudioBufferSourceNode.cpp +++ b/content/media/webaudio/AudioBufferSourceNode.cpp @@ -1,40 +1,220 @@ /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim:set ts=2 sw=2 sts=2 et cindent: */ /* 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/. */ #include "AudioBufferSourceNode.h" #include "mozilla/dom/AudioBufferSourceNodeBinding.h" +#include "nsMathUtils.h" +#include "AudioNodeEngine.h" +#include "AudioNodeStream.h" namespace mozilla { namespace dom { NS_IMPL_CYCLE_COLLECTION_INHERITED_1(AudioBufferSourceNode, AudioSourceNode, mBuffer) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(AudioBufferSourceNode) NS_INTERFACE_MAP_END_INHERITING(AudioSourceNode) NS_IMPL_ADDREF_INHERITED(AudioBufferSourceNode, AudioSourceNode) NS_IMPL_RELEASE_INHERITED(AudioBufferSourceNode, AudioSourceNode) +class AudioBufferSourceNodeEngine : public AudioNodeEngine +{ +public: + AudioBufferSourceNodeEngine() : + mStart(0), mStop(TRACK_TICKS_MAX), mOffset(0), mDuration(0) {} + + // START, OFFSET and DURATION are always set by start() (along with setting + // mBuffer to something non-null). + // STOP is set by stop(). + enum Parameters { + START, + STOP, + OFFSET, + DURATION + }; + virtual void SetStreamTimeParameter(uint32_t aIndex, TrackTicks aParam) + { + switch (aIndex) { + case START: mStart = aParam; break; + case STOP: mStop = aParam; break; + default: + NS_ERROR("Bad AudioBufferSourceNodeEngine StreamTimeParameter"); + } + } + virtual void SetInt32Parameter(uint32_t aIndex, int32_t aParam) + { + switch (aIndex) { + case OFFSET: mOffset = aParam; break; + case DURATION: mDuration = aParam; break; + default: + NS_ERROR("Bad AudioBufferSourceNodeEngine Int32Parameter"); + } + } + virtual void SetBuffer(already_AddRefed<ThreadSharedFloatArrayBufferList> aBuffer) + { + mBuffer = aBuffer; + } + + virtual void ProduceAudioBlock(AudioNodeStream* aStream, + const AudioChunk& aInput, + AudioChunk* aOutput, + bool* aFinished) + { + if (!mBuffer) + return; + TrackTicks currentPosition = aStream->GetCurrentPosition(); + if (currentPosition + WEBAUDIO_BLOCK_SIZE <= mStart) { + aOutput->SetNull(WEBAUDIO_BLOCK_SIZE); + return; + } + TrackTicks endTime = std::min(mStart + mDuration, mStop); + // Don't set *aFinished just because we passed mStop. Maybe someone + // will call stop() again with a different value. + if (currentPosition + WEBAUDIO_BLOCK_SIZE >= mStart + mDuration) { + *aFinished = true; + } + if (currentPosition >= endTime || mStart >= endTime) { + aOutput->SetNull(WEBAUDIO_BLOCK_SIZE); + return; + } + + uint32_t channels = mBuffer->GetChannels(); + if (!channels) { + aOutput->SetNull(WEBAUDIO_BLOCK_SIZE); + return; + } + + if (currentPosition >= mStart && + currentPosition + WEBAUDIO_BLOCK_SIZE <= endTime) { + // Data is entirely within the buffer. Avoid copying it. + aOutput->mDuration = WEBAUDIO_BLOCK_SIZE; + aOutput->mBuffer = mBuffer; + aOutput->mChannelData.SetLength(channels); + for (uint32_t i = 0; i < channels; ++i) { + aOutput->mChannelData[i] = + mBuffer->GetData(i) + uintptr_t(currentPosition - mStart + mOffset); + } + aOutput->mVolume = 1.0f; + aOutput->mBufferFormat = AUDIO_FORMAT_FLOAT32; + return; + } + + AllocateAudioBlock(channels, aOutput); + TrackTicks start = std::max(currentPosition, mStart); + TrackTicks end = std::min(currentPosition + WEBAUDIO_BLOCK_SIZE, endTime); + WriteZeroesToAudioBlock(aOutput, 0, uint32_t(start - currentPosition)); + for (uint32_t i = 0; i < channels; ++i) { + memcpy(static_cast<float*>(const_cast<void*>(aOutput->mChannelData[i])) + + uint32_t(start - currentPosition), + mBuffer->GetData(i) + + uintptr_t(start - mStart + mOffset), + uint32_t(end - start)); + } + uint32_t endOffset = uint32_t(end - currentPosition); + WriteZeroesToAudioBlock(aOutput, endOffset, WEBAUDIO_BLOCK_SIZE - endOffset); + } + + TrackTicks mStart; + TrackTicks mStop; + nsRefPtr<ThreadSharedFloatArrayBufferList> mBuffer; + int32_t mOffset; + int32_t mDuration; +}; + AudioBufferSourceNode::AudioBufferSourceNode(AudioContext* aContext) : AudioSourceNode(aContext) + , mStartCalled(false) { + SetProduceOwnOutput(true); + mStream = aContext->Graph()->CreateAudioNodeStream(new AudioBufferSourceNodeEngine()); + mStream->AddMainThreadListener(this); +} + +AudioBufferSourceNode::~AudioBufferSourceNode() +{ + DestroyMediaStream(); } JSObject* AudioBufferSourceNode::WrapObject(JSContext* aCx, JSObject* aScope) { return AudioBufferSourceNodeBinding::Wrap(aCx, aScope, this); } void -AudioBufferSourceNode::SetBuffer(AudioBuffer* aBuffer) +AudioBufferSourceNode::Start(JSContext* aCx, double aWhen, double aOffset, + const Optional<double>& aDuration, ErrorResult& aRv) { - mBuffer = aBuffer; + if (mStartCalled) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + mStartCalled = true; + + AudioNodeStream* ns = static_cast<AudioNodeStream*>(mStream.get()); + if (!mBuffer || !ns) { + // Nothing to play, or we're already dead for some reason + return; + } + + uint32_t rate = Context()->GetRate(); + uint32_t lengthSamples; + nsRefPtr<ThreadSharedFloatArrayBufferList> data = + mBuffer->GetThreadSharedChannelsForRate(aCx, rate, &lengthSamples); + double length = double(lengthSamples)/rate; + double offset = std::max(0.0, aOffset); + double endOffset = aDuration.WasPassed() ? + std::min(aOffset + aDuration.Value(), length) : length; + if (offset >= endOffset) { + return; + } + + ns->SetBuffer(data.forget()); + // Don't set parameter unnecessarily + if (aWhen > 0.0) { + ns->SetStreamTimeParameter(AudioBufferSourceNodeEngine::START, + Context()->DestinationStream(), + aWhen); + } + int32_t offsetTicks = NS_lround(offset*rate); + // Don't set parameter unnecessarily + if (offsetTicks > 0) { + ns->SetInt32Parameter(AudioBufferSourceNodeEngine::OFFSET, offsetTicks); + } + ns->SetInt32Parameter(AudioBufferSourceNodeEngine::DURATION, + NS_lround(endOffset*rate) - offsetTicks); +} + +void +AudioBufferSourceNode::Stop(double aWhen, ErrorResult& aRv) +{ + if (!mStartCalled) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + AudioNodeStream* ns = static_cast<AudioNodeStream*>(mStream.get()); + if (!ns) { + // We've already stopped and had our stream shut down + return; + } + + ns->SetStreamTimeParameter(AudioBufferSourceNodeEngine::STOP, + Context()->DestinationStream(), + std::max(0.0, aWhen)); +} + +void +AudioBufferSourceNode::NotifyMainThreadStateChanged() +{ + if (mStream->IsFinished()) { + SetProduceOwnOutput(false); + } } } } -
--- a/content/media/webaudio/AudioBufferSourceNode.h +++ b/content/media/webaudio/AudioBufferSourceNode.h @@ -4,40 +4,62 @@ * 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/. */ #ifndef AudioBufferSourceNode_h_ #define AudioBufferSourceNode_h_ #include "AudioSourceNode.h" #include "AudioBuffer.h" +#include "mozilla/dom/BindingUtils.h" namespace mozilla { namespace dom { -class AudioBufferSourceNode : public AudioSourceNode +class AudioBufferSourceNode : public AudioSourceNode, + public MainThreadMediaStreamListener { public: explicit AudioBufferSourceNode(AudioContext* aContext); + virtual ~AudioBufferSourceNode(); + + virtual void DestroyMediaStream() MOZ_OVERRIDE + { + if (mStream) { + mStream->RemoveMainThreadListener(this); + } + AudioSourceNode::DestroyMediaStream(); + } + virtual bool SupportsMediaStreams() const MOZ_OVERRIDE + { + return true; + } NS_DECL_ISUPPORTS_INHERITED NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(AudioBufferSourceNode, AudioSourceNode) virtual JSObject* WrapObject(JSContext* aCx, JSObject* aScope); - void Start(double) { /* no-op for now */ } - void Stop(double) { /* no-op for now */ } + void Start(JSContext* aCx, double aWhen, double aOffset, + const Optional<double>& aDuration, ErrorResult& aRv); + void Stop(double aWhen, ErrorResult& aRv); AudioBuffer* GetBuffer() const { return mBuffer; } - void SetBuffer(AudioBuffer* aBuffer); + void SetBuffer(AudioBuffer* aBuffer) + { + mBuffer = aBuffer; + } + + virtual void NotifyMainThreadStateChanged() MOZ_OVERRIDE; private: nsRefPtr<AudioBuffer> mBuffer; + bool mStartCalled; }; } } #endif
--- a/content/media/webaudio/AudioContext.cpp +++ b/content/media/webaudio/AudioContext.cpp @@ -3,16 +3,17 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "AudioContext.h" #include "nsContentUtils.h" #include "nsIDOMWindow.h" #include "mozilla/ErrorResult.h" +#include "MediaStreamGraph.h" #include "AudioDestinationNode.h" #include "AudioBufferSourceNode.h" #include "AudioBuffer.h" #include "GainNode.h" #include "DelayNode.h" #include "PannerNode.h" #include "AudioListener.h" #include "DynamicsCompressorNode.h" @@ -23,21 +24,24 @@ namespace mozilla { namespace dom { NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_3(AudioContext, mWindow, mDestination, mListener) NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(AudioContext, AddRef) NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(AudioContext, Release) +static uint8_t gWebAudioOutputKey; + AudioContext::AudioContext(nsIDOMWindow* aWindow) : mWindow(aWindow) - , mDestination(new AudioDestinationNode(this)) - , mSampleRate(44100) // hard-code for now + , mDestination(new AudioDestinationNode(this, MediaStreamGraph::GetInstance())) { + // Actually play audio + mDestination->Stream()->AddAudioOutput(&gWebAudioOutputKey); SetIsDOMBinding(); } AudioContext::~AudioContext() { } JSObject* @@ -70,21 +74,28 @@ AudioContext::CreateBufferSource() return bufferNode.forget(); } already_AddRefed<AudioBuffer> AudioContext::CreateBuffer(JSContext* aJSContext, uint32_t aNumberOfChannels, uint32_t aLength, float aSampleRate, ErrorResult& aRv) { - nsRefPtr<AudioBuffer> buffer = new AudioBuffer(this, aLength, aSampleRate); + if (aLength > INT32_MAX) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return nullptr; + } + + nsRefPtr<AudioBuffer> buffer = + new AudioBuffer(this, int32_t(aLength), aSampleRate); if (!buffer->InitializeBuffers(aNumberOfChannels, aJSContext)) { aRv.Throw(NS_ERROR_OUT_OF_MEMORY); return nullptr; } + return buffer.forget(); } already_AddRefed<GainNode> AudioContext::CreateGain() { nsRefPtr<GainNode> gainNode = new GainNode(this); return gainNode.forget(); @@ -159,11 +170,22 @@ AudioContext::DecodeAudioData(const Arra } void AudioContext::RemoveFromDecodeQueue(WebAudioDecodeJob* aDecodeJob) { mDecodeJobs.RemoveElement(aDecodeJob); } +MediaStreamGraph* +AudioContext::Graph() const +{ + return Destination()->Stream()->Graph(); +} + +MediaStream* +AudioContext::DestinationStream() const +{ + return Destination()->Stream(); +} + } } -
--- a/content/media/webaudio/AudioContext.h +++ b/content/media/webaudio/AudioContext.h @@ -12,16 +12,19 @@ #include "mozilla/Attributes.h" #include "nsCOMPtr.h" #include "EnableWebAudioCheck.h" #include "nsAutoPtr.h" #include "mozilla/dom/TypedArray.h" #include "mozilla/dom/BindingUtils.h" #include "mozilla/dom/AudioContextBinding.h" #include "MediaBufferDecoder.h" +#include "StreamBuffer.h" +#include "MediaStreamGraph.h" +#include "nsIDOMWindow.h" struct JSContext; class JSObject; class nsIDOMWindow; namespace mozilla { class ErrorResult; @@ -39,20 +42,19 @@ class DynamicsCompressorNode; class GainNode; class GlobalObject; class PannerNode; class AudioContext MOZ_FINAL : public nsWrapperCache, public EnableWebAudioCheck { explicit AudioContext(nsIDOMWindow* aParentWindow); + ~AudioContext(); public: - virtual ~AudioContext(); - NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(AudioContext) NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS(AudioContext) nsIDOMWindow* GetParentObject() const { return mWindow; } @@ -69,17 +71,17 @@ public: AudioDestinationNode* Destination() const { return mDestination; } float SampleRate() const { - return mSampleRate; + return float(IdealAudioRate()); } AudioListener* Listener(); already_AddRefed<AudioBufferSourceNode> CreateBufferSource(); already_AddRefed<AudioBuffer> CreateBuffer(JSContext* aJSContext, uint32_t aNumberOfChannels, @@ -100,27 +102,31 @@ public: already_AddRefed<BiquadFilterNode> CreateBiquadFilter(); void DecodeAudioData(const ArrayBuffer& aBuffer, DecodeSuccessCallback& aSuccessCallback, const Optional<OwningNonNull<DecodeErrorCallback> >& aFailureCallback); + uint32_t GetRate() const { return IdealAudioRate(); } + + MediaStreamGraph* Graph() const; + MediaStream* DestinationStream() const; + private: void RemoveFromDecodeQueue(WebAudioDecodeJob* aDecodeJob); friend struct ::mozilla::WebAudioDecodeJob; private: nsCOMPtr<nsIDOMWindow> mWindow; nsRefPtr<AudioDestinationNode> mDestination; nsRefPtr<AudioListener> mListener; MediaBufferDecoder mDecoder; nsTArray<nsAutoPtr<WebAudioDecodeJob> > mDecodeJobs; - float mSampleRate; }; } } #endif
--- a/content/media/webaudio/AudioDestinationNode.cpp +++ b/content/media/webaudio/AudioDestinationNode.cpp @@ -1,34 +1,38 @@ /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim:set ts=2 sw=2 sts=2 et cindent: */ /* 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/. */ #include "AudioDestinationNode.h" #include "mozilla/dom/AudioDestinationNodeBinding.h" +#include "AudioNodeEngine.h" +#include "AudioNodeStream.h" +#include "MediaStreamGraph.h" #include "nsContentUtils.h" namespace mozilla { namespace dom { NS_IMPL_ISUPPORTS_INHERITED0(AudioDestinationNode, AudioNode) NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(AudioDestinationNode, AudioNode) NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER NS_IMPL_CYCLE_COLLECTION_UNLINK_END NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(AudioDestinationNode, AudioNode) \ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_CYCLE_COLLECTION_TRACE_WRAPPERCACHE(AudioDestinationNode) -AudioDestinationNode::AudioDestinationNode(AudioContext* aContext) +AudioDestinationNode::AudioDestinationNode(AudioContext* aContext, MediaStreamGraph* aGraph) : AudioNode(aContext) { + mStream = aGraph->CreateAudioNodeStream(new AudioNodeEngine()); SetIsDOMBinding(); } JSObject* AudioDestinationNode::WrapObject(JSContext* aCx, JSObject* aScope, bool* aTriedToWrap) { return AudioDestinationNodeBinding::Wrap(aCx, aScope, this, aTriedToWrap);
--- a/content/media/webaudio/AudioDestinationNode.h +++ b/content/media/webaudio/AudioDestinationNode.h @@ -17,17 +17,17 @@ class AudioContext; /** * Need to have an nsWrapperCache on AudioDestinationNodes since * AudioContext.destination returns them. */ class AudioDestinationNode : public AudioNode, public nsWrapperCache { public: - explicit AudioDestinationNode(AudioContext* aContext); + AudioDestinationNode(AudioContext* aContext, MediaStreamGraph* aGraph); NS_DECL_ISUPPORTS_INHERITED NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(AudioDestinationNode, AudioNode) virtual JSObject* WrapObject(JSContext* aCx, JSObject* aScope, bool* aTriedToWrap);
--- a/content/media/webaudio/AudioNode.cpp +++ b/content/media/webaudio/AudioNode.cpp @@ -3,16 +3,17 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "AudioNode.h" #include "AudioContext.h" #include "nsContentUtils.h" #include "mozilla/ErrorResult.h" +#include "AudioNodeStream.h" namespace mozilla { namespace dom { inline void ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback& aCallback, AudioNode::InputNode& aField, const char* aName, @@ -35,55 +36,123 @@ NS_IMPL_CYCLE_COLLECTION_3(AudioNode, mC NS_IMPL_CYCLE_COLLECTING_ADDREF(AudioNode) NS_IMPL_CYCLE_COLLECTING_RELEASE(AudioNode) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AudioNode) NS_INTERFACE_MAP_ENTRY(nsISupports) NS_INTERFACE_MAP_END AudioNode::AudioNode(AudioContext* aContext) : mContext(aContext) + , mJSBindingFinalized(false) + , mCanProduceOwnOutput(false) + , mOutputEnded(false) { MOZ_ASSERT(aContext); } AudioNode::~AudioNode() { + DestroyMediaStream(); MOZ_ASSERT(mInputNodes.IsEmpty()); MOZ_ASSERT(mOutputNodes.IsEmpty()); } static uint32_t +FindIndexOfNode(const nsTArray<AudioNode::InputNode>& aInputNodes, const AudioNode* aNode) +{ + for (uint32_t i = 0; i < aInputNodes.Length(); ++i) { + if (aInputNodes[i].mInputNode == aNode) { + return i; + } + } + return nsTArray<AudioNode::InputNode>::NoIndex; +} + +static uint32_t FindIndexOfNodeWithPorts(const nsTArray<AudioNode::InputNode>& aInputNodes, const AudioNode* aNode, uint32_t aInputPort, uint32_t aOutputPort) { for (uint32_t i = 0; i < aInputNodes.Length(); ++i) { if (aInputNodes[i].mInputNode == aNode && aInputNodes[i].mInputPort == aInputPort && aInputNodes[i].mOutputPort == aOutputPort) { return i; } } return nsTArray<AudioNode::InputNode>::NoIndex; } void +AudioNode::UpdateOutputEnded() +{ + if (mOutputEnded) { + // Already ended, so nothing to do. + return; + } + if (mCanProduceOwnOutput || + !mInputNodes.IsEmpty() || + (!mJSBindingFinalized && NumberOfInputs() > 0)) { + // This node could still produce output in the future. + return; + } + + mOutputEnded = true; + + // Addref this temporarily so the refcount bumping below doesn't destroy us + // prematurely + nsRefPtr<AudioNode> kungFuDeathGrip = this; + + // The idea here is that we remove connections one by one, and at each step + // the graph is in a valid state. + + // Disconnect inputs. We don't need them anymore. + while (!mInputNodes.IsEmpty()) { + uint32_t i = mInputNodes.Length() - 1; + nsRefPtr<AudioNode> input = mInputNodes[i].mInputNode.forget(); + mInputNodes.RemoveElementAt(i); + NS_ASSERTION(mOutputNodes.Contains(this), "input/output inconsistency"); + input->mOutputNodes.RemoveElement(this); + } + + while (!mOutputNodes.IsEmpty()) { + uint32_t i = mOutputNodes.Length() - 1; + nsRefPtr<AudioNode> output = mOutputNodes[i].forget(); + mOutputNodes.RemoveElementAt(i); + uint32_t inputIndex = FindIndexOfNode(output->mInputNodes, this); + NS_ASSERTION(inputIndex != nsTArray<AudioNode::InputNode>::NoIndex, "input/output inconsistency"); + // It doesn't matter which one we remove, since we're going to remove all + // entries for this node anyway. + output->mInputNodes.RemoveElementAt(inputIndex); + + output->UpdateOutputEnded(); + } + + DestroyMediaStream(); +} + +void AudioNode::Connect(AudioNode& aDestination, uint32_t aOutput, uint32_t aInput, ErrorResult& aRv) { if (aOutput >= NumberOfOutputs() || aInput >= aDestination.NumberOfInputs()) { aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR); return; } if (Context() != aDestination.Context()) { aRv.Throw(NS_ERROR_DOM_SYNTAX_ERR); return; } + if (IsOutputEnded() || aDestination.IsOutputEnded()) { + // No need to connect since we're not going to produce anything other + // than silence. + return; + } if (FindIndexOfNodeWithPorts(aDestination.mInputNodes, this, aInput, aOutput) != nsTArray<AudioNode::InputNode>::NoIndex) { // connection already exists. return; } // The MediaStreamGraph will handle cycle detection. We don't need to do it // here. @@ -91,16 +160,24 @@ AudioNode::Connect(AudioNode& aDestinati // Addref this temporarily so the refcount bumping below doesn't destroy us nsRefPtr<AudioNode> kungFuDeathGrip = this; mOutputNodes.AppendElement(&aDestination); InputNode* input = aDestination.mInputNodes.AppendElement(); input->mInputNode = this; input->mInputPort = aInput; input->mOutputPort = aOutput; + if (SupportsMediaStreams() && aDestination.mStream) { + // Connect streams in the MediaStreamGraph + MOZ_ASSERT(aDestination.mStream->AsProcessedStream()); + ProcessedMediaStream* ps = + static_cast<ProcessedMediaStream*>(aDestination.mStream.get()); + input->mStreamPort = + ps->AllocateInputPort(mStream, MediaInputPort::FLAG_BLOCK_OUTPUT); + } } void AudioNode::Disconnect(uint32_t aOutput, ErrorResult& aRv) { if (aOutput >= NumberOfOutputs()) { aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR); return; @@ -120,12 +197,16 @@ AudioNode::Disconnect(uint32_t aOutput, // others, and it's not correct to remove them all since some of them // could be for different output ports. *outputsToUpdate.AppendElement() = mOutputNodes[i].forget(); mOutputNodes.RemoveElementAt(i); break; } } } + + for (uint32_t i = 0; i < outputsToUpdate.Length(); ++i) { + outputsToUpdate[i]->UpdateOutputEnded(); + } } } }
--- a/content/media/webaudio/AudioNode.h +++ b/content/media/webaudio/AudioNode.h @@ -8,35 +8,80 @@ #define AudioNode_h_ #include "nsCycleCollectionParticipant.h" #include "mozilla/Attributes.h" #include "EnableWebAudioCheck.h" #include "nsAutoPtr.h" #include "nsTArray.h" #include "AudioContext.h" +#include "MediaStreamGraph.h" struct JSContext; namespace mozilla { class ErrorResult; namespace dom { +/** + * The DOM object representing a Web Audio AudioNode. + * + * Each AudioNode has a MediaStream representing the actual + * real-time processing and output of this AudioNode. + * + * We track the incoming and outgoing connections to other AudioNodes. + * All connections are strong and thus rely on cycle collection to break them. + * However, we also track whether an AudioNode is capable of producing output + * in the future. If it isn't, then we break its connections to its inputs + * and outputs, allowing nodes to be immediately disconnected. This + * disconnection is done internally, invisible to DOM users. + * + * We say that a node cannot produce output in the future if it has no inputs + * that can, and it is not producing output itself without any inputs, and + * either it can never have any inputs or it has no JS wrapper. (If it has a + * JS wrapper and can accept inputs, then a new input could be added in + * the future.) + */ class AudioNode : public nsISupports, public EnableWebAudioCheck { public: explicit AudioNode(AudioContext* aContext); virtual ~AudioNode(); + // This should be idempotent (safe to call multiple times). + // This should be called in the destructor of every class that overrides + // this method. + virtual void DestroyMediaStream() + { + if (mStream) { + mStream->Destroy(); + mStream = nullptr; + } + } + + // This method should be overridden to return true in nodes + // which support being hooked up to the Media Stream graph. + virtual bool SupportsMediaStreams() const + { + return false; + } + NS_DECL_CYCLE_COLLECTING_ISUPPORTS NS_DECL_CYCLE_COLLECTION_CLASS(AudioNode) + void JSBindingFinalized() + { + NS_ASSERTION(!mJSBindingFinalized, "JS binding already finalized"); + mJSBindingFinalized = true; + UpdateOutputEnded(); + } + AudioContext* GetParentObject() const { return mContext; } AudioContext* Context() const { return mContext; @@ -48,35 +93,77 @@ public: void Disconnect(uint32_t aOutput, ErrorResult& aRv); // The following two virtual methods must be implemented by each node type // to provide their number of input and output ports. These numbers are // constant for the lifetime of the node. Both default to 1. virtual uint32_t NumberOfInputs() const { return 1; } virtual uint32_t NumberOfOutputs() const { return 1; } + // This could possibly delete 'this'. + void UpdateOutputEnded(); + bool IsOutputEnded() const { return mOutputEnded; } + struct InputNode { + ~InputNode() + { + if (mStreamPort) { + mStreamPort->Destroy(); + } + } + // Strong reference. // May be null if the source node has gone away. nsRefPtr<AudioNode> mInputNode; + nsRefPtr<MediaInputPort> mStreamPort; // The index of the input port this node feeds into. uint32_t mInputPort; // The index of the output port this node comes out of. uint32_t mOutputPort; }; + MediaStream* Stream() { return mStream; } + + // Set this to true when the node can produce its own output even if there + // are no inputs. + void SetProduceOwnOutput(bool aCanProduceOwnOutput) + { + mCanProduceOwnOutput = aCanProduceOwnOutput; + if (!aCanProduceOwnOutput) { + UpdateOutputEnded(); + } + } + +protected: + static void Callback(AudioNode* aNode) { /* not implemented */ } + private: nsRefPtr<AudioContext> mContext; +protected: + // Must be set in the constructor. Must not be null. + // If MaxNumberOfInputs() is > 0, then mStream must be a ProcessedMediaStream. + nsRefPtr<MediaStream> mStream; + +private: // For every InputNode, there is a corresponding entry in mOutputNodes of the // InputNode's mInputNode. nsTArray<InputNode> mInputNodes; // For every mOutputNode entry, there is a corresponding entry in mInputNodes // of the mOutputNode entry. We won't necessarily be able to identify the // exact matching entry, since mOutputNodes doesn't include the port // identifiers and the same node could be connected on multiple ports. nsTArray<nsRefPtr<AudioNode> > mOutputNodes; + // True if the JS binding has been finalized (so script no longer has + // a reference to this node). + bool mJSBindingFinalized; + // True if this node can produce its own output even when all inputs + // have ended their output. + bool mCanProduceOwnOutput; + // True if this node can never produce anything except silence in the future. + // Updated by UpdateOutputEnded(). + bool mOutputEnded; }; } } #endif
--- a/content/media/webaudio/AudioParam.cpp +++ b/content/media/webaudio/AudioParam.cpp @@ -8,27 +8,29 @@ #include "nsContentUtils.h" #include "nsIDOMWindow.h" #include "mozilla/ErrorResult.h" #include "mozilla/dom/AudioParamBinding.h" namespace mozilla { namespace dom { -NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_1(AudioParam, mContext) +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_1(AudioParam, mNode) NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(AudioParam, AddRef) NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(AudioParam, Release) -AudioParam::AudioParam(AudioContext* aContext, +AudioParam::AudioParam(AudioNode* aNode, + AudioParam::CallbackType aCallback, float aDefaultValue, float aMinValue, float aMaxValue) : AudioParamTimeline(aDefaultValue) - , mContext(aContext) + , mNode(aNode) + , mCallback(aCallback) , mDefaultValue(aDefaultValue) , mMinValue(aMinValue) , mMaxValue(aMaxValue) { MOZ_ASSERT(aDefaultValue >= aMinValue); MOZ_ASSERT(aDefaultValue <= aMaxValue); MOZ_ASSERT(aMinValue < aMaxValue); SetIsDOMBinding();
--- a/content/media/webaudio/AudioParam.h +++ b/content/media/webaudio/AudioParam.h @@ -8,59 +8,95 @@ #define AudioParam_h_ #include "AudioEventTimeline.h" #include "nsWrapperCache.h" #include "nsCycleCollectionParticipant.h" #include "nsCOMPtr.h" #include "EnableWebAudioCheck.h" #include "nsAutoPtr.h" -#include "AudioContext.h" +#include "AudioNode.h" #include "mozilla/dom/TypedArray.h" #include "mozilla/Util.h" +#include "mozilla/ErrorResult.h" struct JSContext; class nsIDOMWindow; namespace mozilla { -class ErrorResult; - namespace dom { typedef AudioEventTimeline<ErrorResult> AudioParamTimeline; class AudioParam MOZ_FINAL : public nsWrapperCache, public EnableWebAudioCheck, public AudioParamTimeline { public: - AudioParam(AudioContext* aContext, + typedef void (*CallbackType)(AudioNode*); + + AudioParam(AudioNode* aNode, + CallbackType aCallback, float aDefaultValue, float aMinValue, float aMaxValue); virtual ~AudioParam(); NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(AudioParam) NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS(AudioParam) AudioContext* GetParentObject() const { - return mContext; + return mNode->Context(); } virtual JSObject* WrapObject(JSContext* aCx, JSObject* aScope, bool* aTriedToWrap); // We override SetValueCurveAtTime to convert the Float32Array to the wrapper // object. void SetValueCurveAtTime(JSContext* cx, const Float32Array& aValues, double aStartTime, double aDuration, ErrorResult& aRv) { AudioParamTimeline::SetValueCurveAtTime(aValues.Data(), aValues.Length(), aStartTime, aDuration, aRv); + mCallback(mNode); + } + + // We override the rest of the mutating AudioParamTimeline methods in order to make + // sure that the callback is called every time that this object gets mutated. + void SetValue(float aValue) + { + AudioParamTimeline::SetValue(aValue); + mCallback(mNode); + } + void SetValueAtTime(float aValue, double aStartTime, ErrorResult& aRv) + { + AudioParamTimeline::SetValueAtTime(aValue, aStartTime, aRv); + mCallback(mNode); + } + void LinearRampToValueAtTime(float aValue, double aEndTime, ErrorResult& aRv) + { + AudioParamTimeline::LinearRampToValueAtTime(aValue, aEndTime, aRv); + mCallback(mNode); + } + void ExponentialRampToValueAtTime(float aValue, double aEndTime, ErrorResult& aRv) + { + AudioParamTimeline::ExponentialRampToValueAtTime(aValue, aEndTime, aRv); + mCallback(mNode); + } + void SetTargetAtTime(float aTarget, double aStartTime, double aTimeConstant, ErrorResult& aRv) + { + AudioParamTimeline::SetTargetAtTime(aTarget, aStartTime, aTimeConstant, aRv); + mCallback(mNode); + } + void CancelScheduledValues(double aStartTime) + { + AudioParamTimeline::CancelScheduledValues(aStartTime); + mCallback(mNode); } float MinValue() const { return mMinValue; } float MaxValue() const @@ -69,17 +105,18 @@ public: } float DefaultValue() const { return mDefaultValue; } private: - nsRefPtr<AudioContext> mContext; + nsRefPtr<AudioNode> mNode; + CallbackType mCallback; const float mDefaultValue; const float mMinValue; const float mMaxValue; }; } }
--- a/content/media/webaudio/BiquadFilterNode.cpp +++ b/content/media/webaudio/BiquadFilterNode.cpp @@ -17,27 +17,25 @@ NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_ NS_INTERFACE_MAP_END_INHERITING(AudioNode) NS_IMPL_ADDREF_INHERITED(BiquadFilterNode, AudioNode) NS_IMPL_RELEASE_INHERITED(BiquadFilterNode, AudioNode) static float Nyquist(AudioContext* aContext) { - // TODO: Replace the hardcoded 44100 here with AudioContext::SampleRate() - // when we implement that. - return 0.5f * 44100; + return 0.5f * aContext->SampleRate(); } BiquadFilterNode::BiquadFilterNode(AudioContext* aContext) : AudioNode(aContext) , mType(BiquadTypeEnum::LOWPASS) - , mFrequency(new AudioParam(aContext, 350.f, 10.f, Nyquist(aContext))) - , mQ(new AudioParam(aContext, 1.f, 0.0001f, 1000.f)) - , mGain(new AudioParam(aContext, 0.f, -40.f, 40.f)) + , mFrequency(new AudioParam(this, Callback, 350.f, 10.f, Nyquist(aContext))) + , mQ(new AudioParam(this, Callback, 1.f, 0.0001f, 1000.f)) + , mGain(new AudioParam(this, Callback, 0.f, -40.f, 40.f)) { } JSObject* BiquadFilterNode::WrapObject(JSContext* aCx, JSObject* aScope) { return BiquadFilterNodeBinding::Wrap(aCx, aScope, this); }
--- a/content/media/webaudio/DelayNode.cpp +++ b/content/media/webaudio/DelayNode.cpp @@ -16,17 +16,17 @@ NS_IMPL_CYCLE_COLLECTION_INHERITED_1(Del NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(DelayNode) NS_INTERFACE_MAP_END_INHERITING(AudioNode) NS_IMPL_ADDREF_INHERITED(DelayNode, AudioNode) NS_IMPL_RELEASE_INHERITED(DelayNode, AudioNode) DelayNode::DelayNode(AudioContext* aContext, double aMaxDelay) : AudioNode(aContext) - , mDelay(new AudioParam(aContext, 0.0f, 0.0f, float(aMaxDelay))) + , mDelay(new AudioParam(this, Callback, 0.0f, 0.0f, float(aMaxDelay))) { } JSObject* DelayNode::WrapObject(JSContext* aCx, JSObject* aScope) { return DelayNodeBinding::Wrap(aCx, aScope, this); }
--- a/content/media/webaudio/DynamicsCompressorNode.cpp +++ b/content/media/webaudio/DynamicsCompressorNode.cpp @@ -21,22 +21,22 @@ NS_IMPL_CYCLE_COLLECTION_INHERITED_6(Dyn NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(DynamicsCompressorNode) NS_INTERFACE_MAP_END_INHERITING(AudioNode) NS_IMPL_ADDREF_INHERITED(DynamicsCompressorNode, AudioNode) NS_IMPL_RELEASE_INHERITED(DynamicsCompressorNode, AudioNode) DynamicsCompressorNode::DynamicsCompressorNode(AudioContext* aContext) : AudioNode(aContext) - , mThreshold(new AudioParam(aContext, -24.f, -100.f, 0.f)) - , mKnee(new AudioParam(aContext, 30.f, 0.f, 40.f)) - , mRatio(new AudioParam(aContext, 12.f, 1.f, 20.f)) - , mReduction(new AudioParam(aContext, 0.f, -20.f, 0.f)) - , mAttack(new AudioParam(aContext, 0.003f, 0.f, 1.f)) - , mRelease(new AudioParam(aContext, 0.25f, 0.f, 1.f)) + , mThreshold(new AudioParam(this, Callback, -24.f, -100.f, 0.f)) + , mKnee(new AudioParam(this, Callback, 30.f, 0.f, 40.f)) + , mRatio(new AudioParam(this, Callback, 12.f, 1.f, 20.f)) + , mReduction(new AudioParam(this, Callback, 0.f, -20.f, 0.f)) + , mAttack(new AudioParam(this, Callback, 0.003f, 0.f, 1.f)) + , mRelease(new AudioParam(this, Callback, 0.25f, 0.f, 1.f)) { } JSObject* DynamicsCompressorNode::WrapObject(JSContext* aCx, JSObject* aScope) { return DynamicsCompressorNodeBinding::Wrap(aCx, aScope, this); }
--- a/content/media/webaudio/GainNode.cpp +++ b/content/media/webaudio/GainNode.cpp @@ -16,17 +16,17 @@ NS_IMPL_CYCLE_COLLECTION_INHERITED_1(Gai NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(GainNode) NS_INTERFACE_MAP_END_INHERITING(AudioNode) NS_IMPL_ADDREF_INHERITED(GainNode, AudioNode) NS_IMPL_RELEASE_INHERITED(GainNode, AudioNode) GainNode::GainNode(AudioContext* aContext) : AudioNode(aContext) - , mGain(new AudioParam(aContext, 1.0f, 0.0f, 1.0f)) + , mGain(new AudioParam(this, Callback, 1.0f, 0.0f, 1.0f)) { } JSObject* GainNode::WrapObject(JSContext* aCx, JSObject* aScope) { return GainNodeBinding::Wrap(aCx, aScope, this); }
--- a/content/media/webaudio/Makefile.in +++ b/content/media/webaudio/Makefile.in @@ -60,8 +60,11 @@ PARALLEL_DIRS := test ifdef ENABLE_TESTS TOOL_DIRS += compiledtest endif FORCE_STATIC_LIB := 1 include $(topsrcdir)/config/rules.mk + +CFLAGS += $(GSTREAMER_CFLAGS) +CXXFLAGS += $(GSTREAMER_CFLAGS)
--- a/content/media/webaudio/test/test_AudioBuffer.html +++ b/content/media/webaudio/test/test_AudioBuffer.html @@ -7,23 +7,23 @@ </head> <body> <pre id="test"> <script class="testbody" type="text/javascript"> SimpleTest.waitForExplicitFinish(); addLoadEvent(function() { SpecialPowers.setBoolPref("media.webaudio.enabled", true); - var ac = new AudioContext(); - var buffer = ac.createBuffer(2, 2048, 44100); + var context = new AudioContext(); + var buffer = context.createBuffer(2, 2048, context.sampleRate); SpecialPowers.gc(); // Make sure that our channels are accessible after GC ok(buffer, "Buffer was allocated successfully"); - is(buffer.sampleRate, 44100, "Correct sample rate"); + is(buffer.sampleRate, context.sampleRate, "Correct sample rate"); is(buffer.length, 2048, "Correct length"); - ok(Math.abs(buffer.duration - 2048 / 44100) < 0.0001, "Correct duration"); + ok(Math.abs(buffer.duration - 2048 / context.sampleRate) < 0.0001, "Correct duration"); is(buffer.numberOfChannels, 2, "Correct number of channels"); for (var i = 0; i < buffer.numberOfChannels; ++i) { var buf = buffer.getChannelData(i); ok(buf, "Buffer index " + i + " exists"); ok(buf instanceof Float32Array, "Result is a typed array"); is(buf.length, buffer.length, "Correct length"); var foundNonZero = false; for (var j = 0; j < buf.length; ++j) {
--- a/content/media/webaudio/test/test_AudioContext.html +++ b/content/media/webaudio/test/test_AudioContext.html @@ -17,17 +17,17 @@ addLoadEvent(function() { new AudioContext(); } catch (e) { accessThrows = true; } ok(accessThrows, "AudioContext should be hidden behind a pref"); SpecialPowers.setBoolPref("media.webaudio.enabled", true); var ac = new AudioContext(); ok(ac, "Create a AudioContext object"); - is(ac.sampleRate, 44100, "Correct sample rate"); + is(ac.sampleRate, 48000, "Correct sample rate"); SpecialPowers.clearUserPref("media.webaudio.enabled"); SimpleTest.finish(); }); </script> </pre> </body> </html>
--- a/content/media/webaudio/test/test_biquadFilterNode.html +++ b/content/media/webaudio/test/test_biquadFilterNode.html @@ -14,36 +14,36 @@ function near(a, b, msg) { ok(Math.abs(a - b) < 1e-3, msg); } SimpleTest.waitForExplicitFinish(); addLoadEvent(function() { SpecialPowers.setBoolPref("media.webaudio.enabled", true); var context = new AudioContext(); - var buffer = context.createBuffer(1, 2048, 44100); + var buffer = context.createBuffer(1, 2048, context.sampleRate); for (var i = 0; i < 2048; ++i) { - buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / 44100); + buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate); } var destination = context.destination; var source = context.createBufferSource(); var filter = context.createBiquadFilter(); source.buffer = buffer; source.connect(filter); filter.connect(destination); // Verify default values is(filter.type, 0, "Correct default value for type"); near(filter.frequency.minValue, 10, "Correct min value for filter frequency"); - near(filter.frequency.maxValue, 22050, "Correct max value for filter frequency"); + near(filter.frequency.maxValue, context.sampleRate/2, "Correct max value for filter frequency"); near(filter.frequency.defaultValue, 350, "Correct default value for filter frequency"); near(filter.Q.minValue, 0.001, "Correct min value for filter Q"); near(filter.Q.maxValue, 1000, "Correct max value for filter Q"); near(filter.Q.defaultValue, 1, "Correct default value for filter Q"); near(filter.gain.minValue, -40, "Correct min value for filter gain"); near(filter.gain.maxValue, 40, "Correct max value for filter gain"); near(filter.gain.defaultValue, 0, "Correct default value for filter gain");
--- a/content/media/webaudio/test/test_decodeAudioData.html +++ b/content/media/webaudio/test/test_decodeAudioData.html @@ -249,15 +249,20 @@ expectTypeError(function() { }); expectTypeError(function() { cx.decodeAudioData("buffer", callbackShouldNeverRun, callbackShouldNeverRun); }); expectTypeError(function() { cx.decodeAudioData(new Uint8Array(100), callbackShouldNeverRun, callbackShouldNeverRun); }); -// Now, let's get real! -runNextTest(); +if (cx.sampleRate == 44100) { + // Now, let's get real! + runNextTest(); +} else { + todo(false, "Decoded data tests disabled; context sampleRate " + cx.sampleRate + " not supported"); + SimpleTest.finish(); +} </script> </pre> </body> </html>
--- a/content/media/webaudio/test/test_delayNode.html +++ b/content/media/webaudio/test/test_delayNode.html @@ -10,19 +10,19 @@ <script src="webaudio.js" type="text/javascript"></script> <script class="testbody" type="text/javascript"> SimpleTest.waitForExplicitFinish(); addLoadEvent(function() { SpecialPowers.setBoolPref("media.webaudio.enabled", true); var context = new AudioContext(); - var buffer = context.createBuffer(1, 2048, 44100); + var buffer = context.createBuffer(1, 2048, context.sampleRate); for (var i = 0; i < 2048; ++i) { - buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / 44100); + buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate); } var destination = context.destination; var source = context.createBufferSource(); var delay = context.createDelay();
--- a/content/media/webaudio/test/test_dynamicsCompressorNode.html +++ b/content/media/webaudio/test/test_dynamicsCompressorNode.html @@ -13,19 +13,19 @@ function near(a, b, msg) { ok(Math.abs(a - b) < 1e-4, msg); } SimpleTest.waitForExplicitFinish(); addLoadEvent(function() { SpecialPowers.setBoolPref("media.webaudio.enabled", true); var context = new AudioContext(); - var buffer = context.createBuffer(1, 2048, 44100); + var buffer = context.createBuffer(1, 2048, context.sampleRate); for (var i = 0; i < 2048; ++i) { - buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / 44100); + buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate); } var destination = context.destination; var source = context.createBufferSource(); var compressor = context.createDynamicsCompressor();
--- a/content/media/webaudio/test/test_gainNode.html +++ b/content/media/webaudio/test/test_gainNode.html @@ -9,19 +9,19 @@ <pre id="test"> <script class="testbody" type="text/javascript"> SimpleTest.waitForExplicitFinish(); addLoadEvent(function() { SpecialPowers.setBoolPref("media.webaudio.enabled", true); var context = new AudioContext(); - var buffer = context.createBuffer(1, 2048, 44100); + var buffer = context.createBuffer(1, 2048, context.sampleRate); for (var i = 0; i < 2048; ++i) { - buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / 44100); + buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate); } var destination = context.destination; var source = context.createBufferSource(); var gain = context.createGain();
--- a/content/media/webaudio/test/test_pannerNode.html +++ b/content/media/webaudio/test/test_pannerNode.html @@ -13,19 +13,19 @@ function near(a, b, msg) { ok(Math.abs(a - b) < 1e-4, msg); } SimpleTest.waitForExplicitFinish(); addLoadEvent(function() { SpecialPowers.setBoolPref("media.webaudio.enabled", true); var context = new AudioContext(); - var buffer = context.createBuffer(1, 2048, 44100); + var buffer = context.createBuffer(1, 2048, context.sampleRate); for (var i = 0; i < 2048; ++i) { - buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / 44100); + buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate); } var destination = context.destination; var source = context.createBufferSource(); var panner = context.createPanner();
--- a/content/media/webaudio/test/test_singleSourceDest.html +++ b/content/media/webaudio/test/test_singleSourceDest.html @@ -9,19 +9,19 @@ <pre id="test"> <script class="testbody" type="text/javascript"> SimpleTest.waitForExplicitFinish(); addLoadEvent(function() { SpecialPowers.setBoolPref("media.webaudio.enabled", true); var context = new AudioContext(); - var buffer = context.createBuffer(1, 2048, 44100); + var buffer = context.createBuffer(1, 2048, context.sampleRate); for (var i = 0; i < 2048; ++i) { - buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / 44100); + buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate); } var destination = context.destination; is(destination.context, context, "Destination node has proper context"); is(destination.context, context, "Destination node has proper context"); is(destination.numberOfInputs, 1, "Destination node has 1 inputs"); is(destination.numberOfOutputs, 0, "Destination node has 0 outputs");
--- a/docshell/shistory/src/nsSHistory.cpp +++ b/docshell/shistory/src/nsSHistory.cpp @@ -57,16 +57,18 @@ static PRCList gSHistoryList; // Max viewers allowed total, across all SHistory objects - negative default // means we will calculate how many viewers to cache based on total memory int32_t nsSHistory::sHistoryMaxTotalViewers = -1; // A counter that is used to be able to know the order in which // entries were touched, so that we can evict older entries first. static uint32_t gTouchCounter = 0; +#ifdef PR_LOGGING + static PRLogModuleInfo* GetSHistoryLog() { static PRLogModuleInfo *sLog; if (!sLog) sLog = PR_NewLogModule("nsSHistory"); return sLog; } @@ -100,16 +102,24 @@ GetSHistoryLog() PR_BEGIN_MACRO \ if (PR_LOG_TEST(GetSHistoryLog(), PR_LOG_DEBUG)) { \ nsCOMPtr<nsIURI> uri; \ shentry->GetURI(getter_AddRefs(uri)); \ LOG_SPEC(format, uri); \ } \ PR_END_MACRO +#else // !PR_LOGGING + +#define LOG(format) +#define LOG_SPEC(format, uri) +#define LOG_SHENTRY_SPEC(format, shentry) + +#endif // PR_LOGGING + // Iterates over all registered session history listeners. #define ITERATE_LISTENERS(body) \ PR_BEGIN_MACRO \ { \ nsAutoTObserverArray<nsWeakPtr, 2>::EndLimitedIterator \ iter(mListeners); \ while (iter.HasMore()) { \ nsCOMPtr<nsISHistoryListener> listener = \
--- a/docshell/test/Makefile.in +++ b/docshell/test/Makefile.in @@ -20,18 +20,16 @@ include $(DEPTH)/config/autoconf.mk XPCSHELL_TESTS = unit # FIXME/bug 575918: out-of-process xpcshell is broken on OS X ifneq ($(OS_ARCH),Darwin) XPCSHELL_TESTS += unit_ipc endif MOCHITEST_FILES = \ - test_bug94514.html \ - bug94514-postpage.html \ test_bug123696.html \ bug123696-subframe.html \ test_bug344861.html \ test_bug369814.html \ bug369814.zip \ bug369814.jar \ test_bug384014.html \ test_bug387979.html \
--- a/dom/apps/src/Webapps.js +++ b/dom/apps/src/Webapps.js @@ -476,16 +476,17 @@ WebappsApplication.prototype = { }, launch: function(aStartPoint) { let request = this.createRequest(); cpmm.sendAsyncMessage("Webapps:Launch", { origin: this.origin, manifestURL: this.manifestURL, startPoint: aStartPoint || "", oid: this._id, + timestamp: Date.now(), requestID: this.getRequestId(request) }); return request; }, clearBrowserData: function() { let browserChild = BrowserElementPromptService.getBrowserElementChildForWindow(this._window); if (browserChild) {
--- a/dom/bindings/Bindings.conf +++ b/dom/bindings/Bindings.conf @@ -93,16 +93,17 @@ DOMInterfaces = { 'AudioContext': { 'implicitJSContext': [ 'createBuffer' ], 'nativeOwnership': 'refcounted', 'resultNotAddRefed': [ 'destination', 'listener' ], }, 'AudioBufferSourceNode': { + 'implicitJSContext': [ 'start' ], 'wrapperCache': False }, 'AudioListener' : { 'nativeOwnership': 'refcounted' }, 'AudioNode' : {
--- a/dom/browser-element/BrowserElementScrolling.js +++ b/dom/browser-element/BrowserElementScrolling.js @@ -556,17 +556,17 @@ const ContentPanning = { return (showing > 0.9 && (ratioW > 0.9 || ratioH > 0.9)); } }; ContentPanning.init(); // Min/max velocity of kinetic panning. This is in pixels/millisecond. -const kMinVelocity = 0.4; +const kMinVelocity = 0.2; const kMaxVelocity = 6; // Constants that affect the "friction" of the scroll pane. const kExponentialC = 1000; const kPolynomialC = 100 / 1000000; // How often do we change the position of the scroll pane? // Too often and panning may jerk near the end. @@ -586,37 +586,49 @@ const KineticPanning = { return this.target !== null; }, target: null, start: function kp_start(target) { this.target = target; // Calculate the initial velocity of the movement based on user input - let momentums = this.momentums.slice(-kSamples); + let momentums = this.momentums; + let flick = momentums[momentums.length - 1].time - momentums[0].time < 300; + + // Calculate the panning based on the last moves. + momentums = momentums.slice(-kSamples); let distance = new Point(0, 0); momentums.forEach(function(momentum) { distance.add(momentum.dx, momentum.dy); }); - let elapsed = momentums[momentums.length - 1].time - momentums[0].time; - function clampFromZero(x, min, max) { if (x >= 0) return Math.max(min, Math.min(max, x)); return Math.min(-min, Math.max(-max, x)); } + let elapsed = momentums[momentums.length - 1].time - momentums[0].time; let velocityX = clampFromZero(distance.x / elapsed, 0, kMaxVelocity); let velocityY = clampFromZero(distance.y / elapsed, 0, kMaxVelocity); let velocity = this._velocity; - velocity.set(Math.abs(velocityX) < kMinVelocity ? 0 : velocityX, - Math.abs(velocityY) < kMinVelocity ? 0 : velocityY); + if (flick) { + // Very fast pan action that does not generate a click are very often pan + // action. If this is a small gesture then it will not move the view a lot + // and so it will be above the minimun threshold and not generate any + // kinetic panning. This does not look on a device since this is often + // a real gesture, so let's lower the velocity threshold for such moves. + velocity.set(velocityX, velocityY); + } else { + velocity.set(Math.abs(velocityX) < kMinVelocity ? 0 : velocityX, + Math.abs(velocityY) < kMinVelocity ? 0 : velocityY); + } this.momentums = []; // Set acceleration vector to opposite signs of velocity function sign(x) { return x ? (x > 0 ? 1 : -1) : 0; } this._acceleration.set(velocity.clone().map(sign).scale(-kPolynomialC));
--- a/dom/indexedDB/TransactionThreadPool.cpp +++ b/dom/indexedDB/TransactionThreadPool.cpp @@ -22,42 +22,16 @@ namespace { const uint32_t kThreadLimit = 20; const uint32_t kIdleThreadLimit = 5; const uint32_t kIdleThreadTimeoutMs = 30000; TransactionThreadPool* gInstance = nullptr; bool gShutdown = false; -inline -nsresult -CheckOverlapAndMergeObjectStores(nsTArray<nsString>& aLockedStores, - const nsTArray<nsString>& aObjectStores, - bool aShouldMerge, - bool* aStoresOverlap) -{ - uint32_t length = aObjectStores.Length(); - - bool overlap = false; - - for (uint32_t index = 0; index < length; index++) { - const nsString& storeName = aObjectStores[index]; - if (aLockedStores.Contains(storeName)) { - overlap = true; - } - else if (aShouldMerge && !aLockedStores.AppendElement(storeName)) { - NS_WARNING("Out of memory!"); - return NS_ERROR_OUT_OF_MEMORY; - } - } - - *aStoresOverlap = overlap; - return NS_OK; -} - } // anonymous namespace BEGIN_INDEXEDDB_NAMESPACE class FinishTransactionRunnable MOZ_FINAL : public nsIRunnable { public: NS_DECL_ISUPPORTS @@ -175,16 +149,38 @@ TransactionThreadPool::Cleanup() // And make sure they get processed. rv = NS_ProcessPendingEvents(nullptr); NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; } +// static +PLDHashOperator +TransactionThreadPool::MaybeUnblockTransaction(nsPtrHashKey<TransactionInfo>* aKey, + void* aUserArg) +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + + TransactionInfo* maybeUnblockedInfo = aKey->GetKey(); + TransactionInfo* finishedInfo = static_cast<TransactionInfo*>(aUserArg); + + NS_ASSERTION(maybeUnblockedInfo->blockedOn.Contains(finishedInfo), + "Huh?"); + maybeUnblockedInfo->blockedOn.RemoveEntry(finishedInfo); + if (!maybeUnblockedInfo->blockedOn.Count() && + !maybeUnblockedInfo->transaction->IsAborted()) { + // Let this transaction run. + maybeUnblockedInfo->queue->Unblock(); + } + + return PL_DHASH_NEXT; +} + void TransactionThreadPool::FinishTransaction(IDBTransaction* aTransaction) { NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); NS_ASSERTION(aTransaction, "Null pointer!"); // AddRef here because removing from the hash will call Release. nsRefPtr<IDBTransaction> transaction(aTransaction); @@ -192,265 +188,173 @@ TransactionThreadPool::FinishTransaction nsIAtom* databaseId = aTransaction->mDatabase->Id(); DatabaseTransactionInfo* dbTransactionInfo; if (!mTransactionsInProgress.Get(databaseId, &dbTransactionInfo)) { NS_ERROR("We don't know anyting about this database?!"); return; } - nsTArray<TransactionInfo>& transactionsInProgress = + DatabaseTransactionInfo::TransactionHashtable& transactionsInProgress = dbTransactionInfo->transactions; - uint32_t transactionCount = transactionsInProgress.Length(); + uint32_t transactionCount = transactionsInProgress.Count(); #ifdef DEBUG if (aTransaction->mMode == IDBTransaction::VERSION_CHANGE) { NS_ASSERTION(transactionCount == 1, "More transactions running than should be!"); } #endif if (transactionCount == 1) { #ifdef DEBUG { - TransactionInfo& info = transactionsInProgress[0]; - NS_ASSERTION(info.transaction == aTransaction, "Transaction mismatch!"); + const TransactionInfo* info = transactionsInProgress.Get(aTransaction); + NS_ASSERTION(info->transaction == aTransaction, "Transaction mismatch!"); } #endif mTransactionsInProgress.Remove(databaseId); // See if we need to fire any complete callbacks. uint32_t index = 0; while (index < mCompleteCallbacks.Length()) { if (MaybeFireCallback(mCompleteCallbacks[index])) { mCompleteCallbacks.RemoveElementAt(index); } else { index++; } } + + return; } - else { - // We need to rebuild the locked object store list. - nsTArray<nsString> storesWriting, storesReading; - - for (uint32_t index = 0, count = transactionCount; index < count; index++) { - IDBTransaction* transaction = transactionsInProgress[index].transaction; - if (transaction == aTransaction) { - NS_ASSERTION(count == transactionCount, "More than one match?!"); - - transactionsInProgress.RemoveElementAt(index); - index--; - count--; - - continue; - } - - const nsTArray<nsString>& objectStores = transaction->mObjectStoreNames; + TransactionInfo* info = transactionsInProgress.Get(aTransaction); + NS_ASSERTION(info, "We've never heard of this transaction?!?"); - bool dummy; - if (transaction->mMode == IDBTransaction::READ_WRITE) { - if (NS_FAILED(CheckOverlapAndMergeObjectStores(storesWriting, - objectStores, - true, &dummy))) { - NS_WARNING("Out of memory!"); - } - } - else if (transaction->mMode == IDBTransaction::READ_ONLY) { - if (NS_FAILED(CheckOverlapAndMergeObjectStores(storesReading, - objectStores, - true, &dummy))) { - NS_WARNING("Out of memory!"); - } - } - else { - NS_NOTREACHED("Unknown mode!"); - } + const nsTArray<nsString>& objectStoreNames = aTransaction->mObjectStoreNames; + for (uint32_t index = 0, count = objectStoreNames.Length(); index < count; + index++) { + TransactionInfoPair* blockInfo = + dbTransactionInfo->blockingTransactions.Get(objectStoreNames[index]); + NS_ASSERTION(blockInfo, "Huh?"); + + if (aTransaction->mMode == IDBTransaction::READ_WRITE && + blockInfo->lastBlockingReads == info) { + blockInfo->lastBlockingReads = nullptr; } - NS_ASSERTION(transactionsInProgress.Length() == transactionCount - 1, - "Didn't find the transaction we were looking for!"); - - dbTransactionInfo->storesWriting.SwapElements(storesWriting); - dbTransactionInfo->storesReading.SwapElements(storesReading); + uint32_t i = blockInfo->lastBlockingWrites.IndexOf(info); + if (i != blockInfo->lastBlockingWrites.NoIndex) { + blockInfo->lastBlockingWrites.RemoveElementAt(i); + } } - // Try to dispatch all the queued transactions again. - nsTArray<QueuedDispatchInfo> queuedDispatch; - queuedDispatch.SwapElements(mDelayedDispatchQueue); + info->blocking.EnumerateEntries(MaybeUnblockTransaction, info); - transactionCount = queuedDispatch.Length(); - for (uint32_t index = 0; index < transactionCount; index++) { - if (NS_FAILED(Dispatch(queuedDispatch[index]))) { - NS_WARNING("Dispatch failed!"); - } - } + transactionsInProgress.Remove(aTransaction); } -nsresult -TransactionThreadPool::TransactionCanRun(IDBTransaction* aTransaction, - bool* aCanRun, - TransactionQueue** aExistingQueue) +TransactionThreadPool::TransactionQueue& +TransactionThreadPool::GetQueueForTransaction(IDBTransaction* aTransaction) { NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); NS_ASSERTION(aTransaction, "Null pointer!"); - NS_ASSERTION(aCanRun, "Null pointer!"); - NS_ASSERTION(aExistingQueue, "Null pointer!"); nsIAtom* databaseId = aTransaction->mDatabase->Id(); const nsTArray<nsString>& objectStoreNames = aTransaction->mObjectStoreNames; const uint16_t mode = aTransaction->mMode; // See if we can run this transaction now. DatabaseTransactionInfo* dbTransactionInfo; if (!mTransactionsInProgress.Get(databaseId, &dbTransactionInfo)) { - // First transaction for this database, fine to run. - *aCanRun = true; - *aExistingQueue = nullptr; - return NS_OK; + // First transaction for this database. + dbTransactionInfo = new DatabaseTransactionInfo(); + mTransactionsInProgress.Put(databaseId, dbTransactionInfo); + } + + DatabaseTransactionInfo::TransactionHashtable& transactionsInProgress = + dbTransactionInfo->transactions; + TransactionInfo* info = transactionsInProgress.Get(aTransaction); + if (info) { + // We recognize this one. + return *info->queue; } - nsTArray<TransactionInfo>& transactionsInProgress = - dbTransactionInfo->transactions; + TransactionInfo* transactionInfo = new TransactionInfo(aTransaction, + objectStoreNames); + + dbTransactionInfo->transactions.Put(aTransaction, transactionInfo);; - uint32_t transactionCount = transactionsInProgress.Length(); - NS_ASSERTION(transactionCount, "Should never be 0!"); + for (uint32_t index = 0, count = objectStoreNames.Length(); index < count; + index++) { + TransactionInfoPair* blockInfo = + dbTransactionInfo->blockingTransactions.Get(objectStoreNames[index]); + if (!blockInfo) { + blockInfo = new TransactionInfoPair(); + blockInfo->lastBlockingReads = nullptr; + dbTransactionInfo->blockingTransactions.Put(objectStoreNames[index], + blockInfo); + } - for (uint32_t index = 0; index < transactionCount; index++) { - // See if this transaction is in out list of current transactions. - const TransactionInfo& info = transactionsInProgress[index]; - if (info.transaction == aTransaction) { - *aCanRun = true; - *aExistingQueue = info.queue; - return NS_OK; + // Mark what we are blocking on. + if (blockInfo->lastBlockingReads) { + TransactionInfo* blockingInfo = blockInfo->lastBlockingReads; + transactionInfo->blockedOn.PutEntry(blockingInfo); + blockingInfo->blocking.PutEntry(transactionInfo); + } + + if (mode == IDBTransaction::READ_WRITE && + blockInfo->lastBlockingWrites.Length()) { + for (uint32_t index = 0, + count = blockInfo->lastBlockingWrites.Length(); index < count; + index++) { + TransactionInfo* blockingInfo = blockInfo->lastBlockingWrites[index]; + transactionInfo->blockedOn.PutEntry(blockingInfo); + blockingInfo->blocking.PutEntry(transactionInfo); + } + } + + if (mode == IDBTransaction::READ_WRITE) { + blockInfo->lastBlockingReads = transactionInfo; + blockInfo->lastBlockingWrites.Clear(); + } + else { + blockInfo->lastBlockingWrites.AppendElement(transactionInfo); } } - NS_ASSERTION(mode != IDBTransaction::VERSION_CHANGE, "How did we get here?"); - - bool writeOverlap; - nsresult rv = - CheckOverlapAndMergeObjectStores(dbTransactionInfo->storesWriting, - objectStoreNames, - mode == IDBTransaction::READ_WRITE, - &writeOverlap); - NS_ENSURE_SUCCESS(rv, rv); - - bool readOverlap; - rv = CheckOverlapAndMergeObjectStores(dbTransactionInfo->storesReading, - objectStoreNames, - mode == IDBTransaction::READ_ONLY, - &readOverlap); - NS_ENSURE_SUCCESS(rv, rv); - - if (writeOverlap || - (readOverlap && mode == IDBTransaction::READ_WRITE)) { - *aCanRun = false; - *aExistingQueue = nullptr; - return NS_OK; + if (!transactionInfo->blockedOn.Count()) { + transactionInfo->queue->Unblock(); } - *aCanRun = true; - *aExistingQueue = nullptr; - return NS_OK; + return *transactionInfo->queue; } nsresult TransactionThreadPool::Dispatch(IDBTransaction* aTransaction, nsIRunnable* aRunnable, bool aFinish, nsIRunnable* aFinishRunnable) { NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); NS_ASSERTION(aTransaction, "Null pointer!"); NS_ASSERTION(aRunnable, "Null pointer!"); if (aTransaction->mDatabase->IsInvalidated() && !aFinish) { return NS_ERROR_NOT_AVAILABLE; } - bool canRun; - TransactionQueue* existingQueue; - nsresult rv = TransactionCanRun(aTransaction, &canRun, &existingQueue); - NS_ENSURE_SUCCESS(rv, rv); - - if (!canRun) { - QueuedDispatchInfo* info = mDelayedDispatchQueue.AppendElement(); - NS_ENSURE_TRUE(info, NS_ERROR_OUT_OF_MEMORY); - - info->transaction = aTransaction; - info->runnable = aRunnable; - info->finish = aFinish; - info->finishRunnable = aFinishRunnable; - - return NS_OK; - } - - if (existingQueue) { - existingQueue->Dispatch(aRunnable); - if (aFinish) { - existingQueue->Finish(aFinishRunnable); - } - return NS_OK; - } - - nsIAtom* databaseId = aTransaction->mDatabase->Id(); - -#ifdef DEBUG - if (aTransaction->mMode == IDBTransaction::VERSION_CHANGE) { - NS_ASSERTION(!mTransactionsInProgress.Get(databaseId, nullptr), - "Shouldn't have anything in progress!"); - } -#endif - - DatabaseTransactionInfo* dbTransactionInfo; - nsAutoPtr<DatabaseTransactionInfo> autoDBTransactionInfo; + TransactionQueue& queue = GetQueueForTransaction(aTransaction); - if (!mTransactionsInProgress.Get(databaseId, &dbTransactionInfo)) { - // Make a new struct for this transaction. - autoDBTransactionInfo = new DatabaseTransactionInfo(); - dbTransactionInfo = autoDBTransactionInfo; - } - - const nsTArray<nsString>& objectStoreNames = aTransaction->mObjectStoreNames; - - nsTArray<nsString>& storesInUse = - aTransaction->mMode == IDBTransaction::READ_WRITE ? - dbTransactionInfo->storesWriting : - dbTransactionInfo->storesReading; - - if (!storesInUse.AppendElements(objectStoreNames)) { - NS_WARNING("Out of memory!"); - return NS_ERROR_OUT_OF_MEMORY; + queue.Dispatch(aRunnable); + if (aFinish) { + queue.Finish(aFinishRunnable); } - - nsTArray<TransactionInfo>& transactionInfoArray = - dbTransactionInfo->transactions; - - TransactionInfo* transactionInfo = transactionInfoArray.AppendElement(); - NS_ENSURE_TRUE(transactionInfo, NS_ERROR_OUT_OF_MEMORY); - - transactionInfo->transaction = aTransaction; - transactionInfo->queue = new TransactionQueue(aTransaction, aRunnable); - if (aFinish) { - transactionInfo->queue->Finish(aFinishRunnable); - } - - if (!transactionInfo->objectStoreNames.AppendElements(objectStoreNames)) { - NS_WARNING("Out of memory!"); - return NS_ERROR_OUT_OF_MEMORY; - } - - if (autoDBTransactionInfo) { - mTransactionsInProgress.Put(databaseId, autoDBTransactionInfo); - autoDBTransactionInfo.forget(); - } - - return mThreadPool->Dispatch(transactionInfo->queue, NS_DISPATCH_NORMAL); + return NS_OK; } bool TransactionThreadPool::WaitForAllDatabasesToComplete( nsTArray<IDBDatabase*>& aDatabases, nsIRunnable* aCallback) { NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); @@ -470,91 +374,107 @@ TransactionThreadPool::WaitForAllDatabas if (MaybeFireCallback(*callback)) { mCompleteCallbacks.RemoveElementAt(mCompleteCallbacks.Length() - 1); } return true; } +// static +PLDHashOperator +TransactionThreadPool::CollectTransactions(IDBTransaction* aKey, + TransactionInfo* aValue, + void* aUserArg) +{ + nsAutoTArray<nsRefPtr<IDBTransaction>, 50>* transactionArray = + static_cast<nsAutoTArray<nsRefPtr<IDBTransaction>, 50>*>(aUserArg); + transactionArray->AppendElement(aKey); + + return PL_DHASH_NEXT; +} + void TransactionThreadPool::AbortTransactionsForDatabase(IDBDatabase* aDatabase) { NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); NS_ASSERTION(aDatabase, "Null pointer!"); // Get list of transactions for this database id DatabaseTransactionInfo* dbTransactionInfo; if (!mTransactionsInProgress.Get(aDatabase->Id(), &dbTransactionInfo)) { - // If there are no running transactions, there can't be any pending ones + // If there are no transactions, we're done. return; } - nsAutoTArray<nsRefPtr<IDBTransaction>, 50> transactions; - // Collect any running transactions - nsTArray<TransactionInfo>& transactionsInProgress = + DatabaseTransactionInfo::TransactionHashtable& transactionsInProgress = dbTransactionInfo->transactions; - uint32_t transactionCount = transactionsInProgress.Length(); - NS_ASSERTION(transactionCount, "Should never be 0!"); + NS_ASSERTION(transactionsInProgress.Count(), "Should never be 0!"); - for (uint32_t index = 0; index < transactionCount; index++) { - // See if any transaction belongs to this IDBDatabase instance - IDBTransaction* transaction = transactionsInProgress[index].transaction; - if (transaction->Database() == aDatabase) { - transactions.AppendElement(transaction); - } - } - - // Collect any pending transactions. - for (uint32_t index = 0; index < mDelayedDispatchQueue.Length(); index++) { - // See if any transaction belongs to this IDBDatabase instance - IDBTransaction* transaction = mDelayedDispatchQueue[index].transaction; - if (transaction->Database() == aDatabase) { - transactions.AppendElement(transaction); - } - } + nsAutoTArray<nsRefPtr<IDBTransaction>, 50> transactions; + transactionsInProgress.EnumerateRead(CollectTransactions, &transactions); // Abort transactions. Do this after collecting the transactions in case // calling Abort() modifies the data structures we're iterating above. for (uint32_t index = 0; index < transactions.Length(); index++) { + if (transactions[index]->Database() != aDatabase) { + continue; + } + // This can fail, for example if the transaction is in the process of // being comitted. That is expected and fine, so we ignore any returned // errors. transactions[index]->Abort(); } } +struct NS_STACK_CLASS TransactionSearchInfo +{ + TransactionSearchInfo(IDBDatabase* aDatabase) + : db(aDatabase), found(false) + { + } + + IDBDatabase* db; + bool found; +}; + +// static +PLDHashOperator +TransactionThreadPool::FindTransaction(IDBTransaction* aKey, + TransactionInfo* aValue, + void* aUserArg) +{ + TransactionSearchInfo* info = static_cast<TransactionSearchInfo*>(aUserArg); + + if (aKey->Database() == info->db) { + info->found = true; + return PL_DHASH_STOP; + } + + return PL_DHASH_NEXT; +} bool TransactionThreadPool::HasTransactionsForDatabase(IDBDatabase* aDatabase) { NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); NS_ASSERTION(aDatabase, "Null pointer!"); - // Get list of transactions for this database id - DatabaseTransactionInfo* dbTransactionInfo; - if (!mTransactionsInProgress.Get(aDatabase->Id(), &dbTransactionInfo)) { + DatabaseTransactionInfo* dbTransactionInfo = nullptr; + dbTransactionInfo = mTransactionsInProgress.Get(aDatabase->Id()); + if (!dbTransactionInfo) { return false; } - nsTArray<TransactionInfo>& transactionsInProgress = - dbTransactionInfo->transactions; - - uint32_t transactionCount = transactionsInProgress.Length(); - NS_ASSERTION(transactionCount, "Should never be 0!"); + TransactionSearchInfo info(aDatabase); + dbTransactionInfo->transactions.EnumerateRead(FindTransaction, &info); - for (uint32_t index = 0; index < transactionCount; index++) { - // See if any transaction belongs to this IDBDatabase instance - if (transactionsInProgress[index].transaction->Database() == aDatabase) { - return true; - } - } - - return false; + return info.found; } bool TransactionThreadPool::MaybeFireCallback(DatabasesCompleteCallback& aCallback) { NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); for (uint32_t index = 0; index < aCallback.mDatabases.Length(); index++) { @@ -564,25 +484,33 @@ TransactionThreadPool::MaybeFireCallback } } aCallback.mCallback->Run(); return true; } TransactionThreadPool:: -TransactionQueue::TransactionQueue(IDBTransaction* aTransaction, - nsIRunnable* aRunnable) +TransactionQueue::TransactionQueue(IDBTransaction* aTransaction) : mMonitor("TransactionQueue::mMonitor"), mTransaction(aTransaction), mShouldFinish(false) { NS_ASSERTION(aTransaction, "Null pointer!"); - NS_ASSERTION(aRunnable, "Null pointer!"); - mQueue.AppendElement(aRunnable); +} + +void +TransactionThreadPool::TransactionQueue::Unblock() +{ + MonitorAutoLock lock(mMonitor); + + // NB: Finish may be called before Unblock. + + TransactionThreadPool::Get()->mThreadPool-> + Dispatch(this, NS_DISPATCH_NORMAL); } void TransactionThreadPool::TransactionQueue::Dispatch(nsIRunnable* aRunnable) { MonitorAutoLock lock(mMonitor); NS_ASSERTION(!mShouldFinish, "Dispatch called after Finish!"); @@ -610,17 +538,17 @@ NS_IMPL_THREADSAFE_ISUPPORTS1(Transactio NS_IMETHODIMP TransactionThreadPool::TransactionQueue::Run() { nsAutoTArray<nsCOMPtr<nsIRunnable>, 10> queue; nsCOMPtr<nsIRunnable> finishRunnable; bool shouldFinish = false; - while(!shouldFinish) { + do { NS_ASSERTION(queue.IsEmpty(), "Should have cleared this!"); { MonitorAutoLock lock(mMonitor); while (!mShouldFinish && mQueue.IsEmpty()) { if (NS_FAILED(mMonitor.Wait())) { NS_ERROR("Failed to wait!"); } @@ -638,17 +566,17 @@ TransactionThreadPool::TransactionQueue: nsCOMPtr<nsIRunnable>& runnable = queue[index]; runnable->Run(); runnable = nullptr; } if (count) { queue.Clear(); } - } + } while (!shouldFinish); nsCOMPtr<nsIRunnable> finishTransactionRunnable = new FinishTransactionRunnable(mTransaction, finishRunnable); if (NS_FAILED(NS_DispatchToMainThread(finishTransactionRunnable, NS_DISPATCH_NORMAL))) { NS_WARNING("Failed to dispatch finishTransactionRunnable!"); }
--- a/dom/indexedDB/TransactionThreadPool.h +++ b/dom/indexedDB/TransactionThreadPool.h @@ -58,88 +58,135 @@ public: protected: class TransactionQueue MOZ_FINAL : public nsIRunnable { public: NS_DECL_ISUPPORTS NS_DECL_NSIRUNNABLE - inline TransactionQueue(IDBTransaction* aTransaction, - nsIRunnable* aRunnable); + TransactionQueue(IDBTransaction* aTransaction); + + void Unblock(); - inline void Dispatch(nsIRunnable* aRunnable); + void Dispatch(nsIRunnable* aRunnable); - inline void Finish(nsIRunnable* aFinishRunnable); + void Finish(nsIRunnable* aFinishRunnable); private: mozilla::Monitor mMonitor; IDBTransaction* mTransaction; nsAutoTArray<nsCOMPtr<nsIRunnable>, 10> mQueue; nsCOMPtr<nsIRunnable> mFinishRunnable; bool mShouldFinish; }; + friend class TransactionQueue; + struct TransactionInfo { + TransactionInfo(IDBTransaction* aTransaction, + const nsTArray<nsString>& aObjectStoreNames) + { + MOZ_COUNT_CTOR(TransactionInfo); + + blockedOn.Init(); + blocking.Init(); + + transaction = aTransaction; + queue = new TransactionQueue(aTransaction); + objectStoreNames.AppendElements(aObjectStoreNames); + } + + ~TransactionInfo() + { + MOZ_COUNT_DTOR(TransactionInfo); + } + nsRefPtr<IDBTransaction> transaction; nsRefPtr<TransactionQueue> queue; nsTArray<nsString> objectStoreNames; + nsTHashtable<nsPtrHashKey<TransactionInfo> > blockedOn; + nsTHashtable<nsPtrHashKey<TransactionInfo> > blocking; + }; + + struct TransactionInfoPair + { + TransactionInfoPair() + : lastBlockingReads(nullptr) + { + MOZ_COUNT_CTOR(TransactionInfoPair); + } + + ~TransactionInfoPair() + { + MOZ_COUNT_DTOR(TransactionInfoPair); + } + // Multiple reading transactions can block future writes. + nsTArray<TransactionInfo*> lastBlockingWrites; + // But only a single writing transaction can block future reads. + TransactionInfo* lastBlockingReads; }; struct DatabaseTransactionInfo { - nsTArray<TransactionInfo> transactions; - nsTArray<nsString> storesReading; - nsTArray<nsString> storesWriting; + DatabaseTransactionInfo() + { + MOZ_COUNT_CTOR(DatabaseTransactionInfo); + + transactions.Init(); + blockingTransactions.Init(); + } + + ~DatabaseTransactionInfo() + { + MOZ_COUNT_DTOR(DatabaseTransactionInfo); + } + + typedef nsClassHashtable<nsPtrHashKey<IDBTransaction>, TransactionInfo > + TransactionHashtable; + TransactionHashtable transactions; + nsClassHashtable<nsStringHashKey, TransactionInfoPair> blockingTransactions; }; - struct QueuedDispatchInfo - { - QueuedDispatchInfo() - : finish(false) - { } + static PLDHashOperator + CollectTransactions(IDBTransaction* aKey, + TransactionInfo* aValue, + void* aUserArg); - nsRefPtr<IDBTransaction> transaction; - nsCOMPtr<nsIRunnable> runnable; - nsCOMPtr<nsIRunnable> finishRunnable; - bool finish; - }; + static PLDHashOperator + FindTransaction(IDBTransaction* aKey, + TransactionInfo* aValue, + void* aUserArg); + + static PLDHashOperator + MaybeUnblockTransaction(nsPtrHashKey<TransactionInfo>* aKey, + void* aUserArg); struct DatabasesCompleteCallback { nsTArray<IDBDatabase*> mDatabases; nsCOMPtr<nsIRunnable> mCallback; }; TransactionThreadPool(); ~TransactionThreadPool(); nsresult Init(); nsresult Cleanup(); void FinishTransaction(IDBTransaction* aTransaction); - nsresult TransactionCanRun(IDBTransaction* aTransaction, - bool* aCanRun, - TransactionQueue** aExistingQueue); - - nsresult Dispatch(const QueuedDispatchInfo& aInfo) - { - return Dispatch(aInfo.transaction, aInfo.runnable, aInfo.finish, - aInfo.finishRunnable); - } + TransactionQueue& GetQueueForTransaction(IDBTransaction* aTransaction); bool MaybeFireCallback(DatabasesCompleteCallback& aCallback); nsCOMPtr<nsIThreadPool> mThreadPool; nsClassHashtable<nsISupportsHashKey, DatabaseTransactionInfo> mTransactionsInProgress; - nsTArray<QueuedDispatchInfo> mDelayedDispatchQueue; - nsTArray<DatabasesCompleteCallback> mCompleteCallbacks; }; END_INDEXEDDB_NAMESPACE #endif // mozilla_dom_indexeddb_transactionthreadpool_h__
--- a/dom/ipc/Blob.cpp +++ b/dom/ipc/Blob.cpp @@ -16,17 +16,16 @@ #include "nsIIPCSerializableInputStream.h" #include "nsIRemoteBlob.h" #include "nsISeekableStream.h" #include "mozilla/Monitor.h" #include "mozilla/unused.h" #include "mozilla/ipc/InputStreamUtils.h" #include "nsDOMFile.h" -#include "nsDOMBlobBuilder.h" #include "nsThreadUtils.h" #include "ContentChild.h" #include "ContentParent.h" #define PRIVATE_REMOTE_INPUT_STREAM_IID \ {0x30c7699f, 0x51d2, 0x48c8, {0xad, 0x56, 0xc0, 0x16, 0xd7, 0x6f, 0x71, 0x27}} @@ -626,61 +625,42 @@ BlobTraits<Parent>::BaseType::NoteRunnab return; } } MOZ_NOT_REACHED("Runnable not in our array!"); } template <ActorFlavorEnum ActorFlavor> -class RemoteBlobBase : public nsIRemoteBlob +struct RemoteBlobBase; + +template <> +struct RemoteBlobBase<Parent> +{ + InputStreamParams mInputStreamParams; +}; + +template <> +struct RemoteBlobBase<Child> +{ }; + +template <ActorFlavorEnum ActorFlavor> +class RemoteBlob : public RemoteBlobBase<ActorFlavor>, + public nsDOMFile, + public nsIRemoteBlob { public: typedef RemoteBlob<ActorFlavor> SelfType; typedef Blob<ActorFlavor> ActorType; typedef InputStreamActor<ActorFlavor> StreamActorType; - - void - SetPBlob(void* aActor) MOZ_OVERRIDE - { - MOZ_ASSERT(!aActor || !mActor); - mActor = static_cast<ActorType*>(aActor); - } - - virtual void* - GetPBlob() MOZ_OVERRIDE - { - return static_cast<typename ActorType::ProtocolType*>(mActor); - } + typedef typename ActorType::ConstructorParamsType ConstructorParamsType; private: ActorType* mActor; -protected: - RemoteBlobBase() - : mActor(nullptr) - { } - - virtual - ~RemoteBlobBase() - { - if (mActor) { - mActor->NoteDyingRemoteBlob(); - } - } - - ActorType* Actor() const { return mActor; } -}; - -template <ActorFlavorEnum ActorFlavor> -class RemoteBlob : public nsDOMFile, - public RemoteBlobBase<ActorFlavor> -{ - typedef Blob<ActorFlavor> ActorType; -public: class StreamHelper : public nsRunnable { typedef Blob<ActorFlavor> ActorType; typedef InputStreamActor<ActorFlavor> StreamActorType; mozilla::Monitor mMonitor; ActorType* mActor; nsCOMPtr<nsIDOMBlob> mSourceBlob; @@ -787,17 +767,16 @@ public: public: SliceHelper(ActorType* aActor) : mMonitor("RemoteBlob::SliceHelper::mMonitor"), mActor(aActor), mStart(0), mLength(0), mDone(false) { // This may be created on any thread. MOZ_ASSERT(aActor); - MOZ_ASSERT(aActor->HasManager()); } nsresult GetSlice(uint64_t aStart, uint64_t aLength, const nsAString& aContentType, nsIDOMBlob** aSlice) { // This may be called on any thread. MOZ_ASSERT(aSlice); @@ -854,30 +833,33 @@ public: MOZ_ASSERT(mActor); MOZ_ASSERT(!mSlice); MOZ_ASSERT(!mDone); NormalBlobConstructorParams normalParams; normalParams.contentType() = mContentType; normalParams.length() = mLength; - BlobConstructorNoMultipartParams params(normalParams); + typename ActorType::ConstructorParamsType params; + ActorType::BaseType::SetBlobConstructorParams(params, normalParams); ActorType* newActor = ActorType::Create(params); MOZ_ASSERT(newActor); - mActor->PropagateManager(newActor); SlicedBlobConstructorParams slicedParams; slicedParams.contentType() = mContentType; slicedParams.begin() = mStart; slicedParams.end() = mStart + mLength; SetBlobOnParams(mActor, slicedParams); - BlobConstructorNoMultipartParams params2(slicedParams); - if (mActor->ConstructPBlobOnManager(newActor, params2)) { + typename ActorType::OtherSideConstructorParamsType otherSideParams; + ActorType::BaseType::SetBlobConstructorParams(otherSideParams, + slicedParams); + + if (mActor->Manager()->SendPBlobConstructor(newActor, otherSideParams)) { mSlice = newActor->GetBlob(); } mActor = nullptr; if (aNotify) { MonitorAutoLock lock(mMonitor); mDone = true; @@ -889,156 +871,76 @@ public: } }; public: NS_DECL_ISUPPORTS_INHERITED RemoteBlob(const nsAString& aName, const nsAString& aContentType, uint64_t aLength, uint64_t aModDate) - : nsDOMFile(aName, aContentType, aLength, aModDate) - { - mImmutable = true; - } - - RemoteBlob(const nsAString& aName, const nsAString& aContentType, - uint64_t aLength) - : nsDOMFile(aName, aContentType, aLength) + : nsDOMFile(aName, aContentType, aLength, aModDate), mActor(nullptr) { mImmutable = true; } RemoteBlob(const nsAString& aContentType, uint64_t aLength) - : nsDOMFile(aContentType, aLength) + : nsDOMFile(aContentType, aLength), mActor(nullptr) { mImmutable = true; } RemoteBlob() : nsDOMFile(EmptyString(), EmptyString(), UINT64_MAX, UINT64_MAX) + , mActor(nullptr) { mImmutable = true; } + virtual ~RemoteBlob() + { + if (mActor) { + mActor->NoteDyingRemoteBlob(); + } + } + + void + SetActor(ActorType* aActor) + { + MOZ_ASSERT(!aActor || !mActor); + mActor = aActor; + } + + void + MaybeSetInputStream(const ConstructorParamsType& aParams); + virtual already_AddRefed<nsIDOMBlob> CreateSlice(uint64_t aStart, uint64_t aLength, const nsAString& aContentType) MOZ_OVERRIDE { - ActorType* actor = RemoteBlobBase<ActorFlavor>::Actor(); - if (!actor) { + if (!mActor) { return nullptr; } - nsRefPtr<SliceHelper> helper = new SliceHelper(actor); + nsRefPtr<SliceHelper> helper = new SliceHelper(mActor); nsCOMPtr<nsIDOMBlob> slice; nsresult rv = helper->GetSlice(aStart, aLength, aContentType, getter_AddRefs(slice)); NS_ENSURE_SUCCESS(rv, nullptr); return slice.forget(); } NS_IMETHOD - GetInternalStream(nsIInputStream** aStream) MOZ_OVERRIDE - { - ActorType* actor = RemoteBlobBase<ActorFlavor>::Actor(); - if (!actor) { - return NS_ERROR_UNEXPECTED; - } - - nsRefPtr<StreamHelper> helper = new StreamHelper(actor, this); - return helper->GetStream(aStream); - } - - NS_IMETHOD - GetLastModifiedDate(JSContext* cx, JS::Value* aLastModifiedDate) - { - if (IsDateUnknown()) { - aLastModifiedDate->setNull(); - } else { - JSObject* date = JS_NewDateObjectMsec(cx, mLastModificationDate); - if (!date) { - return NS_ERROR_OUT_OF_MEMORY; - } - aLastModifiedDate->setObject(*date); - } - return NS_OK; - } -}; - -template <ActorFlavorEnum ActorFlavor> -class RemoteMemoryBlob : public nsDOMMemoryFile, - public RemoteBlobBase<ActorFlavor> -{ -public: - NS_DECL_ISUPPORTS_INHERITED - - RemoteMemoryBlob(void* aMemoryBuffer, - uint64_t aLength, - const nsAString& aName, - const nsAString& aContentType, - uint64_t aModDate) - : nsDOMMemoryFile(aMemoryBuffer, aLength, aName, aContentType, aModDate) - { - mImmutable = true; - } + GetInternalStream(nsIInputStream** aStream) MOZ_OVERRIDE; - RemoteMemoryBlob(void* aMemoryBuffer, - uint64_t aLength, - const nsAString& aName, - const nsAString& aContentType) - : nsDOMMemoryFile(aMemoryBuffer, aLength, aName, aContentType) - { - mImmutable = true; - } - - RemoteMemoryBlob(void* aMemoryBuffer, - uint64_t aLength, - const nsAString& aContentType) - : nsDOMMemoryFile(aMemoryBuffer, aLength, EmptyString(), aContentType) - { - mImmutable = true; - } - - NS_IMETHOD - GetLastModifiedDate(JSContext* cx, JS::Value* aLastModifiedDate) + virtual void* + GetPBlob() MOZ_OVERRIDE { - if (IsDateUnknown()) { - aLastModifiedDate->setNull(); - } else { - JSObject* date = JS_NewDateObjectMsec(cx, mLastModificationDate); - if (!date) { - return NS_ERROR_OUT_OF_MEMORY; - } - aLastModifiedDate->setObject(*date); - } - return NS_OK; - } -}; - -template <ActorFlavorEnum ActorFlavor> -class RemoteMultipartBlob : public nsDOMMultipartFile, - public RemoteBlobBase<ActorFlavor> -{ - -public: - NS_DECL_ISUPPORTS_INHERITED - - RemoteMultipartBlob(const nsAString& aName, - const nsAString& aContentType) - : nsDOMMultipartFile(aName, aContentType) - { - mImmutable = true; - } - - RemoteMultipartBlob(const nsAString& aName) - : nsDOMMultipartFile(aName) - { - mImmutable = true; + return static_cast<typename ActorType::ProtocolType*>(mActor); } NS_IMETHOD GetLastModifiedDate(JSContext* cx, JS::Value* aLastModifiedDate) { if (IsDateUnknown()) { aLastModifiedDate->setNull(); } else { @@ -1047,255 +949,138 @@ public: return NS_ERROR_OUT_OF_MEMORY; } aLastModifiedDate->setObject(*date); } return NS_OK; } }; +template <> +void +RemoteBlob<Parent>::MaybeSetInputStream(const ConstructorParamsType& aParams) +{ + if (aParams.optionalInputStreamParams().type() == + OptionalInputStreamParams::TInputStreamParams) { + mInputStreamParams = + aParams.optionalInputStreamParams().get_InputStreamParams(); + } + else { + MOZ_ASSERT(aParams.blobParams().type() == + ChildBlobConstructorParams::TMysteryBlobConstructorParams); + } +} + +template <> +void +RemoteBlob<Child>::MaybeSetInputStream(const ConstructorParamsType& aParams) +{ + // Nothing needed on the child side! +} + +template <> +NS_IMETHODIMP +RemoteBlob<Parent>::GetInternalStream(nsIInputStream** aStream) +{ + if (mInputStreamParams.type() != InputStreamParams::T__None) { + nsCOMPtr<nsIInputStream> stream = DeserializeInputStream(mInputStreamParams); + if (!stream) { + NS_WARNING("Failed to deserialize stream!"); + return NS_ERROR_UNEXPECTED; + } + + stream.forget(aStream); + return NS_OK; + } + + if (!mActor) { + return NS_ERROR_UNEXPECTED; + } + + nsRefPtr<StreamHelper> helper = new StreamHelper(mActor, this); + return helper->GetStream(aStream); +} + +template <> +NS_IMETHODIMP +RemoteBlob<Child>::GetInternalStream(nsIInputStream** aStream) +{ + if (!mActor) { + return NS_ERROR_UNEXPECTED; + } + + nsRefPtr<StreamHelper> helper = new StreamHelper(mActor, this); + return helper->GetStream(aStream); +} + template <ActorFlavorEnum ActorFlavor> Blob<ActorFlavor>::Blob(nsIDOMBlob* aBlob) -: mBlob(aBlob), mRemoteBlob(nullptr), mRemoteMemoryBlob(nullptr), - mRemoteMultipartBlob(nullptr), mContentManager(nullptr), - mBlobManager(nullptr), mOwnsBlob(true), mBlobIsFile(false) +: mBlob(aBlob), mRemoteBlob(nullptr), mOwnsBlob(true), mBlobIsFile(false) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aBlob); aBlob->AddRef(); nsCOMPtr<nsIDOMFile> file = do_QueryInterface(aBlob); mBlobIsFile = !!file; } template <ActorFlavorEnum ActorFlavor> -Blob<ActorFlavor>::Blob(nsRefPtr<RemoteBlobType>& aBlob, - bool aBlobIsFile) -: mBlob(nullptr), mRemoteBlob(nullptr), mRemoteMemoryBlob(nullptr), - mRemoteMultipartBlob(nullptr), mContentManager(nullptr), - mBlobManager(nullptr), mOwnsBlob(true), mBlobIsFile(aBlobIsFile) -{ - MOZ_ASSERT(NS_IsMainThread()); - MOZ_ASSERT(!mBlob); - MOZ_ASSERT(!mRemoteBlob); - MOZ_ASSERT(aBlob); - - if (NS_FAILED(aBlob->SetMutable(false))) { - MOZ_NOT_REACHED("Failed to make remote blob immutable!"); - } - - aBlob->SetPBlob(this); - aBlob.forget(&mRemoteBlob); - - mBlob = mRemoteBlob; -} - -template <ActorFlavorEnum ActorFlavor> -Blob<ActorFlavor>::Blob(nsRefPtr<RemoteMemoryBlobType>& aBlob, - bool aBlobIsFile) -: mBlob(nullptr), mRemoteBlob(nullptr), mRemoteMemoryBlob(nullptr), - mRemoteMultipartBlob(nullptr), mContentManager(nullptr), - mBlobManager(nullptr), mOwnsBlob(true), mBlobIsFile(aBlobIsFile) +Blob<ActorFlavor>::Blob(const ConstructorParamsType& aParams) +: mBlob(nullptr), mRemoteBlob(nullptr), mOwnsBlob(false), mBlobIsFile(false) { MOZ_ASSERT(NS_IsMainThread()); - MOZ_ASSERT(!mBlob); - MOZ_ASSERT(!mRemoteMemoryBlob); - MOZ_ASSERT(aBlob); + + ChildBlobConstructorParams::Type paramType = + BaseType::GetBlobConstructorParams(aParams).type(); - if (NS_FAILED(aBlob->SetMutable(false))) { - MOZ_NOT_REACHED("Failed to make remote blob immutable!"); - } - - aBlob->SetPBlob(this); - aBlob.forget(&mRemoteMemoryBlob); - - mBlob = mRemoteMemoryBlob; -} + mBlobIsFile = + paramType == ChildBlobConstructorParams::TFileBlobConstructorParams || + paramType == ChildBlobConstructorParams::TMysteryBlobConstructorParams; -template <ActorFlavorEnum ActorFlavor> -Blob<ActorFlavor>::Blob(nsRefPtr<RemoteMultipartBlobType>& aBlob, - bool aBlobIsFile) -: mBlob(nullptr), mRemoteBlob(nullptr), mRemoteMemoryBlob(nullptr), - mRemoteMultipartBlob(nullptr), mContentManager(nullptr), - mBlobManager(nullptr), mOwnsBlob(true), mBlobIsFile(aBlobIsFile) -{ - MOZ_ASSERT(NS_IsMainThread()); - MOZ_ASSERT(!mBlob); - MOZ_ASSERT(!mRemoteMultipartBlob); - MOZ_ASSERT(aBlob); + nsRefPtr<RemoteBlobType> remoteBlob = CreateRemoteBlob(aParams); + MOZ_ASSERT(remoteBlob); - if (NS_FAILED(aBlob->SetMutable(false))) { - MOZ_NOT_REACHED("Failed to make remote blob immutable!"); - } + remoteBlob->SetActor(this); + remoteBlob->MaybeSetInputStream(aParams); + remoteBlob.forget(&mRemoteBlob); - aBlob->SetPBlob(this); - aBlob.forget(&mRemoteMultipartBlob); - - mBlob = mRemoteMultipartBlob; + mBlob = mRemoteBlob; + mOwnsBlob = true; } template <ActorFlavorEnum ActorFlavor> Blob<ActorFlavor>* -Blob<ActorFlavor>::Create(const BlobConstructorParams& aParams) +Blob<ActorFlavor>::Create(const ConstructorParamsType& aParams) { MOZ_ASSERT(NS_IsMainThread()); - switch (aParams.type()) { - case BlobConstructorParams::TBlobConstructorNoMultipartParams: { - const BlobConstructorNoMultipartParams& params = - aParams.get_BlobConstructorNoMultipartParams(); - - switch (params.type()) { - case BlobConstructorNoMultipartParams::TBlobOrFileConstructorParams: { - const BlobOrFileConstructorParams& fileParams = - params.get_BlobOrFileConstructorParams(); - nsRefPtr<RemoteBlobType> remoteBlob; - bool isFile = false; - - switch (fileParams.type()) { - case BlobOrFileConstructorParams::TNormalBlobConstructorParams: { - const NormalBlobConstructorParams& normalParams = - fileParams.get_NormalBlobConstructorParams(); - remoteBlob = new RemoteBlobType(normalParams.contentType(), - normalParams.length()); - break; - } - - case BlobOrFileConstructorParams::TFileBlobConstructorParams: { - const FileBlobConstructorParams& fbParams = - fileParams.get_FileBlobConstructorParams(); - remoteBlob = - new RemoteBlobType(fbParams.name(), fbParams.contentType(), - fbParams.length(), fbParams.modDate()); - isFile = true; - break; - } - - default: - MOZ_NOT_REACHED("Unknown params!"); - } + const ChildBlobConstructorParams& blobParams = + BaseType::GetBlobConstructorParams(aParams); - return new Blob<ActorFlavor>(remoteBlob, isFile); - } - case BlobConstructorNoMultipartParams::TMemoryBlobOrFileConstructorParams: { - const MemoryBlobOrFileConstructorParams& memoryParams = - params.get_MemoryBlobOrFileConstructorParams(); - const BlobOrFileConstructorParams& internalParams = - memoryParams.constructorParams(); - nsRefPtr<RemoteMemoryBlobType> remoteMemoryBlob; - bool isFile = false; - - switch (internalParams.type()) { - case BlobOrFileConstructorParams::TNormalBlobConstructorParams: { - const NormalBlobConstructorParams& normalParams = - internalParams.get_NormalBlobConstructorParams(); - MOZ_ASSERT(normalParams.length() == memoryParams.data().Length()); - - void* data = - BlobTraits<ActorFlavor>::Allocate(memoryParams.data().Length()); - if (!data) { - return nullptr; - } - memcpy(data, - memoryParams.data().Elements(), - memoryParams.data().Length()); - remoteMemoryBlob = - new RemoteMemoryBlobType(data, - memoryParams.data().Length(), - normalParams.contentType()); - break; - } - - case BlobOrFileConstructorParams::TFileBlobConstructorParams: { - const FileBlobConstructorParams& fbParams = - internalParams.get_FileBlobConstructorParams(); - MOZ_ASSERT(fbParams.length() == memoryParams.data().Length()); + switch (blobParams.type()) { + case ChildBlobConstructorParams::TNormalBlobConstructorParams: + case ChildBlobConstructorParams::TFileBlobConstructorParams: + case ChildBlobConstructorParams::TMysteryBlobConstructorParams: + return new Blob<ActorFlavor>(aParams); - void* data = - BlobTraits<ActorFlavor>::Allocate(memoryParams.data().Length()); - if (!data) { - return nullptr; - } - memcpy(data, - memoryParams.data().Elements(), - memoryParams.data().Length()); - remoteMemoryBlob = - new RemoteMemoryBlobType(data, - memoryParams.data().Length(), - fbParams.name(), - fbParams.contentType(), - fbParams.modDate()); - isFile = true; - break; - } + case ChildBlobConstructorParams::TSlicedBlobConstructorParams: { + const SlicedBlobConstructorParams& params = + blobParams.get_SlicedBlobConstructorParams(); - default: - MOZ_NOT_REACHED("Unknown params!"); - } - - return new Blob<ActorFlavor>(remoteMemoryBlob, isFile); - } - case BlobConstructorNoMultipartParams::TMysteryBlobConstructorParams: { - nsRefPtr<RemoteBlobType> remoteBlob = new RemoteBlobType(); - return new Blob<ActorFlavor>(remoteBlob, true); - } - - case BlobConstructorNoMultipartParams::TSlicedBlobConstructorParams: { - const SlicedBlobConstructorParams& slicedParams = - params.get_SlicedBlobConstructorParams(); - - nsCOMPtr<nsIDOMBlob> source = - GetBlobFromParams<ActorFlavor>(slicedParams); - MOZ_ASSERT(source); + nsCOMPtr<nsIDOMBlob> source = GetBlobFromParams<ActorFlavor>(params); + MOZ_ASSERT(source); - nsCOMPtr<nsIDOMBlob> slice; - nsresult rv = - source->Slice(slicedParams.begin(), slicedParams.end(), - slicedParams.contentType(), 3, - getter_AddRefs(slice)); - NS_ENSURE_SUCCESS(rv, nullptr); - - return new Blob<ActorFlavor>(slice); - } - - default: - MOZ_NOT_REACHED("Unknown params!"); - } - } - - case BlobConstructorParams::TMultipartBlobOrFileConstructorParams: { - const MultipartBlobOrFileConstructorParams& params = - aParams.get_MultipartBlobOrFileConstructorParams(); + nsCOMPtr<nsIDOMBlob> slice; + nsresult rv = + source->Slice(params.begin(), params.end(), params.contentType(), 3, + getter_AddRefs(slice)); + NS_ENSURE_SUCCESS(rv, nullptr); - nsRefPtr<RemoteMultipartBlobType> file; - bool isFile = false; - const BlobOrFileConstructorParams& internalParams = - params.constructorParams(); - switch (internalParams.type()) { - case BlobOrFileConstructorParams::TNormalBlobConstructorParams: { - const NormalBlobConstructorParams& normalParams = - internalParams.get_NormalBlobConstructorParams(); - file = new RemoteMultipartBlobType(normalParams.contentType()); - break; - } - - case BlobOrFileConstructorParams::TFileBlobConstructorParams: { - const FileBlobConstructorParams& fbParams = - internalParams.get_FileBlobConstructorParams(); - file = new RemoteMultipartBlobType(fbParams.name(), - fbParams.contentType()); - isFile = true; - break; - } - - default: - MOZ_NOT_REACHED("Unknown params!"); - } - - return new Blob<ActorFlavor>(file, isFile); + return new Blob<ActorFlavor>(slice); } default: MOZ_NOT_REACHED("Unknown params!"); } return nullptr; } @@ -1307,18 +1092,17 @@ Blob<ActorFlavor>::GetBlob() MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(mBlob); nsCOMPtr<nsIDOMBlob> blob; // Remote blobs are held alive until the first call to GetBlob. Thereafter we // only hold a weak reference. Normal blobs are held alive until the actor is // destroyed. - if (mOwnsBlob && - (mRemoteBlob || mRemoteMemoryBlob || mRemoteMultipartBlob)) { + if (mRemoteBlob && mOwnsBlob) { blob = dont_AddRef(mBlob); mOwnsBlob = false; } else { blob = mBlob; } MOZ_ASSERT(blob); @@ -1363,71 +1147,67 @@ Blob<ActorFlavor>::SetMysteryBlobInfo(co ToConcreteBlob(mBlob)->SetLazyData(voidString, aContentType, aLength, UINT64_MAX); NormalBlobConstructorParams params(aContentType, aLength); return ProtocolType::SendResolveMystery(params); } template <ActorFlavorEnum ActorFlavor> -typename Blob<ActorFlavor>::ProtocolType* -Blob<ActorFlavor>::ConstructPBlobOnManager(ProtocolType* aActor, - const BlobConstructorParams& aParams) +already_AddRefed<typename Blob<ActorFlavor>::RemoteBlobType> +Blob<ActorFlavor>::CreateRemoteBlob(const ConstructorParamsType& aParams) { MOZ_ASSERT(NS_IsMainThread()); - MOZ_ASSERT(HasManager()); + + const ChildBlobConstructorParams& blobParams = + BaseType::GetBlobConstructorParams(aParams); + + nsRefPtr<RemoteBlobType> remoteBlob; + + switch (blobParams.type()) { + case ChildBlobConstructorParams::TNormalBlobConstructorParams: { + const NormalBlobConstructorParams& params = + blobParams.get_NormalBlobConstructorParams(); + remoteBlob = new RemoteBlobType(params.contentType(), params.length()); + break; + } - if (mContentManager) { - return mContentManager->SendPBlobConstructor(aActor, aParams); - } + case ChildBlobConstructorParams::TFileBlobConstructorParams: { + const FileBlobConstructorParams& params = + blobParams.get_FileBlobConstructorParams(); + remoteBlob = + new RemoteBlobType(params.name(), params.contentType(), + params.length(), params.modDate()); + break; + } - if (mBlobManager) { - return mBlobManager->SendPBlobConstructor(aActor, aParams); + case ChildBlobConstructorParams::TMysteryBlobConstructorParams: { + remoteBlob = new RemoteBlobType(); + break; + } + + default: + MOZ_NOT_REACHED("Unknown params!"); } - MOZ_NOT_REACHED("Why don't I have a manager?!"); - return nullptr; -} - -template <ActorFlavorEnum ActorFlavor> -void -Blob<ActorFlavor>::SetManager(ContentManagerType* aManager) -{ - MOZ_ASSERT(!mContentManager && !mBlobManager); - MOZ_ASSERT(aManager); - - mContentManager = aManager; -} + MOZ_ASSERT(remoteBlob); -template <ActorFlavorEnum ActorFlavor> -void -Blob<ActorFlavor>::SetManager(BlobManagerType* aManager) -{ - MOZ_ASSERT(!mContentManager && !mBlobManager); - MOZ_ASSERT(aManager); + if (NS_FAILED(remoteBlob->SetMutable(false))) { + MOZ_NOT_REACHED("Failed to make remote blob immutable!"); + } - mBlobManager = aManager; -} - -template <ActorFlavorEnum ActorFlavor> -void -Blob<ActorFlavor>::PropagateManager(Blob<ActorFlavor>* aActor) const -{ - MOZ_ASSERT(HasManager()); - - aActor->mContentManager = mContentManager; - aActor->mBlobManager = mBlobManager; + return remoteBlob.forget(); } template <ActorFlavorEnum ActorFlavor> void Blob<ActorFlavor>::NoteDyingRemoteBlob() { MOZ_ASSERT(mBlob); - MOZ_ASSERT(mRemoteBlob || mRemoteMemoryBlob || mRemoteMultipartBlob); + MOZ_ASSERT(mRemoteBlob); MOZ_ASSERT(!mOwnsBlob); // This may be called on any thread due to the fact that RemoteBlob is // designed to be passed between threads. We must start the shutdown process // on the main thread, so we proxy here if necessary. if (!NS_IsMainThread()) { nsCOMPtr<nsIRunnable> runnable = NS_NewNonOwningRunnableMethod(this, @@ -1437,55 +1217,43 @@ Blob<ActorFlavor>::NoteDyingRemoteBlob() } return; } // Must do this before calling Send__delete__ or we'll crash there trying to // access a dangling pointer. mRemoteBlob = nullptr; - mRemoteMemoryBlob = nullptr; - mRemoteMultipartBlob = nullptr; mozilla::unused << ProtocolType::Send__delete__(this); } template <ActorFlavorEnum ActorFlavor> void Blob<ActorFlavor>::ActorDestroy(ActorDestroyReason aWhy) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(mBlob); if (mRemoteBlob) { - mRemoteBlob->SetPBlob(nullptr); - } - - if (mRemoteMemoryBlob) { - mRemoteMemoryBlob->SetPBlob(nullptr); - } - - if (mRemoteMultipartBlob) { - mRemoteMultipartBlob->SetPBlob(nullptr); + mRemoteBlob->SetActor(nullptr); } if (mOwnsBlob) { mBlob->Release(); } } template <ActorFlavorEnum ActorFlavor> bool Blob<ActorFlavor>::RecvResolveMystery(const ResolveMysteryParams& aParams) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(mBlob); MOZ_ASSERT(!mRemoteBlob); - MOZ_ASSERT(!mRemoteMemoryBlob); - MOZ_ASSERT(!mRemoteMultipartBlob); MOZ_ASSERT(mOwnsBlob); if (!mBlobIsFile) { MOZ_ASSERT(false, "Must always be a file!"); return false; } nsDOMFileBase* blob = ToConcreteBlob(mBlob); @@ -1518,18 +1286,16 @@ Blob<ActorFlavor>::RecvResolveMystery(co template <> bool Blob<Parent>::RecvPBlobStreamConstructor(StreamType* aActor) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(mBlob); MOZ_ASSERT(!mRemoteBlob); - MOZ_ASSERT(!mRemoteMemoryBlob); - MOZ_ASSERT(!mRemoteMultipartBlob); nsCOMPtr<nsIInputStream> stream; nsresult rv = mBlob->GetInternalStream(getter_AddRefs(stream)); NS_ENSURE_SUCCESS(rv, false); nsCOMPtr<nsIIPCSerializableInputStream> serializableStream; nsCOMPtr<nsIRemoteBlob> remoteBlob = do_QueryInterface(mBlob); @@ -1575,18 +1341,16 @@ Blob<Parent>::RecvPBlobStreamConstructor template <> bool Blob<Child>::RecvPBlobStreamConstructor(StreamType* aActor) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(mBlob); MOZ_ASSERT(!mRemoteBlob); - MOZ_ASSERT(!mRemoteMemoryBlob); - MOZ_ASSERT(!mRemoteMultipartBlob); nsCOMPtr<nsIInputStream> stream; nsresult rv = mBlob->GetInternalStream(getter_AddRefs(stream)); NS_ENSURE_SUCCESS(rv, false); nsCOMPtr<nsIIPCSerializableInputStream> serializable = do_QueryInterface(stream); if (!serializable) { @@ -1598,35 +1362,16 @@ Blob<Child>::RecvPBlobStreamConstructor( serializable->Serialize(params); MOZ_ASSERT(params.type() != InputStreamParams::T__None); return aActor->Send__delete__(aActor, params); } template <ActorFlavorEnum ActorFlavor> -bool -Blob<ActorFlavor>::RecvPBlobConstructor(ProtocolType* aActor, - const BlobConstructorParams& aParams) -{ - MOZ_ASSERT(NS_IsMainThread()); - - Blob<ActorFlavor>* subBlobActor = static_cast<Blob<ActorFlavor>*>(aActor); - - if (!subBlobActor->ManagerIs(this)) { - // Somebody screwed up! - return false; - } - - nsCOMPtr<nsIDOMBlob> blob = subBlobActor->GetBlob(); - static_cast<nsDOMMultipartFile*>(mBlob)->AddBlob(blob); - return true; -} - -template <ActorFlavorEnum ActorFlavor> typename Blob<ActorFlavor>::StreamType* Blob<ActorFlavor>::AllocPBlobStream() { MOZ_ASSERT(NS_IsMainThread()); return new InputStreamActor<ActorFlavor>(); } template <ActorFlavorEnum ActorFlavor> @@ -1634,61 +1379,24 @@ bool Blob<ActorFlavor>::DeallocPBlobStream(StreamType* aActor) { MOZ_ASSERT(NS_IsMainThread()); delete aActor; return true; } template <ActorFlavorEnum ActorFlavor> -typename Blob<ActorFlavor>::ProtocolType* -Blob<ActorFlavor>::AllocPBlob(const BlobConstructorParams& aParams) -{ - Blob<ActorFlavor>* actor = Blob<ActorFlavor>::Create(aParams); - actor->SetManager(this); - return actor; -} - -template <ActorFlavorEnum ActorFlavor> -bool -Blob<ActorFlavor>::DeallocPBlob(ProtocolType* aActor) -{ - delete aActor; - return true; -} - -template <ActorFlavorEnum ActorFlavor> NS_IMPL_ADDREF_INHERITED(RemoteBlob<ActorFlavor>, nsDOMFile) template <ActorFlavorEnum ActorFlavor> NS_IMPL_RELEASE_INHERITED(RemoteBlob<ActorFlavor>, nsDOMFile) template <ActorFlavorEnum ActorFlavor> NS_IMPL_QUERY_INTERFACE_INHERITED1(RemoteBlob<ActorFlavor>, nsDOMFile, nsIRemoteBlob) -template <ActorFlavorEnum ActorFlavor> -NS_IMPL_ADDREF_INHERITED(RemoteMemoryBlob<ActorFlavor>, nsDOMMemoryFile) - -template <ActorFlavorEnum ActorFlavor> -NS_IMPL_RELEASE_INHERITED(RemoteMemoryBlob<ActorFlavor>, nsDOMMemoryFile) - -template <ActorFlavorEnum ActorFlavor> -NS_IMPL_QUERY_INTERFACE_INHERITED1(RemoteMemoryBlob<ActorFlavor>, nsDOMMemoryFile, - nsIRemoteBlob) - -template <ActorFlavorEnum ActorFlavor> -NS_IMPL_ADDREF_INHERITED(RemoteMultipartBlob<ActorFlavor>, nsDOMMultipartFile) - -template <ActorFlavorEnum ActorFlavor> -NS_IMPL_RELEASE_INHERITED(RemoteMultipartBlob<ActorFlavor>, nsDOMMultipartFile) - -template <ActorFlavorEnum ActorFlavor> -NS_IMPL_QUERY_INTERFACE_INHERITED1(RemoteMultipartBlob<ActorFlavor>, nsDOMMultipartFile, - nsIRemoteBlob) - // Explicit instantiation of both classes. template class Blob<Parent>; template class Blob<Child>; } // namespace ipc } // namespace dom } // namespace mozilla
--- a/dom/ipc/Blob.h +++ b/dom/ipc/Blob.h @@ -33,229 +33,204 @@ namespace ipc { enum ActorFlavorEnum { Parent = 0, Child }; template <ActorFlavorEnum> -struct BlobTraits -{ }; +struct BlobTraits; template <> struct BlobTraits<Parent> { typedef mozilla::dom::PBlobParent ProtocolType; typedef mozilla::dom::PBlobStreamParent StreamType; - typedef mozilla::dom::PContentParent ContentManagerType; + typedef mozilla::dom::ParentBlobConstructorParams ConstructorParamsType; + typedef mozilla::dom::ChildBlobConstructorParams + OtherSideConstructorParamsType; typedef mozilla::dom::ContentParent ConcreteContentManagerType; - typedef ProtocolType BlobManagerType; // BaseType on the parent side is a bit more complicated than for the child // side. In the case of nsIInputStreams backed by files we need to ensure that // the files are actually opened and closed on a background thread before we // can send their file handles across to the child. The child process could // crash during this process so we need to make sure we cancel the intended // response in such a case. We do that by holding an array of // nsRevocableEventPtr. If the child crashes then this actor will be destroyed // and the nsRevocableEventPtr destructor will cancel any stream events that // are currently in flight. class BaseType : public ProtocolType { + public: + static const ChildBlobConstructorParams& + GetBlobConstructorParams(const ConstructorParamsType& aParams) + { + return aParams.blobParams(); + } + + static void + SetBlobConstructorParams(ConstructorParamsType& aParams, + const ChildBlobConstructorParams& aBlobParams) + { + aParams.blobParams() = aBlobParams; + aParams.optionalInputStreamParams() = mozilla::void_t(); + } + + static void + SetBlobConstructorParams(OtherSideConstructorParamsType& aParams, + const ChildBlobConstructorParams& aBlobParams) + { + aParams = aBlobParams; + } + protected: BaseType(); virtual ~BaseType(); class OpenStreamRunnable; friend class OpenStreamRunnable; void NoteRunnableCompleted(OpenStreamRunnable* aRunnable); nsTArray<nsRevocableEventPtr<OpenStreamRunnable> > mOpenStreamRunnables; }; - - static void* - Allocate(size_t aSize) - { - // We want fallible allocation in the parent. - return moz_malloc(aSize); - } }; template <> struct BlobTraits<Child> { typedef mozilla::dom::PBlobChild ProtocolType; typedef mozilla::dom::PBlobStreamChild StreamType; - typedef mozilla::dom::PContentChild ContentManagerType; + typedef mozilla::dom::ChildBlobConstructorParams ConstructorParamsType; + typedef mozilla::dom::ParentBlobConstructorParams + OtherSideConstructorParamsType; typedef mozilla::dom::ContentChild ConcreteContentManagerType; - typedef ProtocolType BlobManagerType; + class BaseType : public ProtocolType { + public: + static const ChildBlobConstructorParams& + GetBlobConstructorParams(const ConstructorParamsType& aParams) + { + return aParams; + } + + static void + SetBlobConstructorParams(ConstructorParamsType& aParams, + const ChildBlobConstructorParams& aBlobParams) + { + aParams = aBlobParams; + } + + static void + SetBlobConstructorParams(OtherSideConstructorParamsType& aParams, + const ChildBlobConstructorParams& aBlobParams) + { + aParams.blobParams() = aBlobParams; + aParams.optionalInputStreamParams() = mozilla::void_t(); + } + protected: BaseType() { } virtual ~BaseType() { } }; - - static void* - Allocate(size_t aSize) - { - // We want infallible allocation in the child. - return moz_xmalloc(aSize); - } }; template <ActorFlavorEnum> -class RemoteBlobBase; -template <ActorFlavorEnum> class RemoteBlob; -template <ActorFlavorEnum> -class RemoteMemoryBlob; -template <ActorFlavorEnum> -class RemoteMultipartBlob; template <ActorFlavorEnum ActorFlavor> class Blob : public BlobTraits<ActorFlavor>::BaseType { - friend class RemoteBlobBase<ActorFlavor>; + friend class RemoteBlob<ActorFlavor>; public: typedef typename BlobTraits<ActorFlavor>::ProtocolType ProtocolType; typedef typename BlobTraits<ActorFlavor>::StreamType StreamType; + typedef typename BlobTraits<ActorFlavor>::ConstructorParamsType + ConstructorParamsType; + typedef typename BlobTraits<ActorFlavor>::OtherSideConstructorParamsType + OtherSideConstructorParamsType; typedef typename BlobTraits<ActorFlavor>::BaseType BaseType; - typedef typename BlobTraits<ActorFlavor>::ContentManagerType ContentManagerType; - typedef typename BlobTraits<ActorFlavor>::BlobManagerType BlobManagerType; typedef RemoteBlob<ActorFlavor> RemoteBlobType; - typedef RemoteMemoryBlob<ActorFlavor> RemoteMemoryBlobType; - typedef RemoteMultipartBlob<ActorFlavor> RemoteMultipartBlobType; typedef mozilla::ipc::IProtocolManager< mozilla::ipc::RPCChannel::RPCListener>::ActorDestroyReason ActorDestroyReason; - typedef mozilla::dom::BlobConstructorParams BlobConstructorParams; protected: nsIDOMBlob* mBlob; RemoteBlobType* mRemoteBlob; - RemoteMemoryBlobType* mRemoteMemoryBlob; - RemoteMultipartBlobType* mRemoteMultipartBlob; - - ContentManagerType* mContentManager; - BlobManagerType* mBlobManager; - bool mOwnsBlob; bool mBlobIsFile; public: // This create function is called on the sending side. static Blob* Create(nsIDOMBlob* aBlob) { return new Blob(aBlob); } // This create function is called on the receiving side. static Blob* - Create(const BlobConstructorParams& aParams); + Create(const ConstructorParamsType& aParams); // Get the blob associated with this actor. This may always be called on the // sending side. It may also be called on the receiving side unless this is a // "mystery" blob that has not yet received a SetMysteryBlobInfo() call. already_AddRefed<nsIDOMBlob> GetBlob(); // Use this for files. bool SetMysteryBlobInfo(const nsString& aName, const nsString& aContentType, uint64_t aLength, uint64_t aLastModifiedDate); // Use this for non-file blobs. bool SetMysteryBlobInfo(const nsString& aContentType, uint64_t aLength); - ProtocolType* - ConstructPBlobOnManager(ProtocolType* aActor, - const BlobConstructorParams& aParams); - - bool - ManagerIs(const ContentManagerType* aManager) const - { - return aManager == mContentManager; - } - - bool - ManagerIs(const BlobManagerType* aManager) const - { - return aManager == mBlobManager; - } - -#ifdef DEBUG - bool - HasManager() const - { - return !!mContentManager || !!mBlobManager; - } -#endif - - void - SetManager(ContentManagerType* aManager); - void - SetManager(BlobManagerType* aManager); - - void - PropagateManager(Blob<ActorFlavor>* aActor) const; - private: // This constructor is called on the sending side. Blob(nsIDOMBlob* aBlob); - // These constructors are called on the receiving side. - Blob(nsRefPtr<RemoteBlobType>& aBlob, - bool aIsFile); - Blob(nsRefPtr<RemoteMemoryBlobType>& aBlob, - bool aIsFile); - Blob(nsRefPtr<RemoteMultipartBlobType>& aBlob, - bool aIsFile); + // This constructor is called on the receiving side. + Blob(const ConstructorParamsType& aParams); + + static already_AddRefed<RemoteBlobType> + CreateRemoteBlob(const ConstructorParamsType& aParams); void NoteDyingRemoteBlob(); // These methods are only called by the IPDL message machinery. virtual void ActorDestroy(ActorDestroyReason aWhy) MOZ_OVERRIDE; virtual bool RecvResolveMystery(const ResolveMysteryParams& aParams) MOZ_OVERRIDE; virtual bool RecvPBlobStreamConstructor(StreamType* aActor) MOZ_OVERRIDE; - virtual bool - RecvPBlobConstructor(ProtocolType* aActor, - const BlobConstructorParams& aParams) MOZ_OVERRIDE; - virtual StreamType* AllocPBlobStream() MOZ_OVERRIDE; virtual bool DeallocPBlobStream(StreamType* aActor) MOZ_OVERRIDE; - - virtual ProtocolType* - AllocPBlob(const BlobConstructorParams& params) MOZ_OVERRIDE; - - virtual bool - DeallocPBlob(ProtocolType* actor) MOZ_OVERRIDE; }; } // namespace ipc typedef mozilla::dom::ipc::Blob<mozilla::dom::ipc::Child> BlobChild; typedef mozilla::dom::ipc::Blob<mozilla::dom::ipc::Parent> BlobParent; } // namespace dom } // namespace mozilla -#endif // mozilla_dom_ipc_Blob_h +#endif // mozilla_dom_ipc_Blob_h \ No newline at end of file
--- a/dom/ipc/ContentChild.cpp +++ b/dom/ipc/ContentChild.cpp @@ -86,16 +86,17 @@ #ifdef ACCESSIBILITY #include "nsIAccessibilityService.h" #endif #include "mozilla/dom/indexedDB/PIndexedDBChild.h" #include "mozilla/dom/sms/SmsChild.h" #include "mozilla/dom/devicestorage/DeviceStorageRequestChild.h" #include "mozilla/dom/bluetooth/PBluetoothChild.h" +#include "mozilla/ipc/InputStreamUtils.h" #include "nsDOMFile.h" #include "nsIRemoteBlob.h" #include "ProcessUtils.h" #include "StructuredCloneUtils.h" #include "URIUtils.h" #include "nsIScriptSecurityManager.h" #include "nsContentUtils.h" @@ -557,114 +558,36 @@ ContentChild::DeallocPBrowser(PBrowserCh TabChild* child = static_cast<TabChild*>(iframe); NS_RELEASE(child); return true; } PBlobChild* ContentChild::AllocPBlob(const BlobConstructorParams& aParams) { - BlobChild* actor = BlobChild::Create(aParams); - actor->SetManager(this); - return actor; + return BlobChild::Create(aParams); } bool ContentChild::DeallocPBlob(PBlobChild* aActor) { delete aActor; return true; } -bool -ContentChild::GetParamsForBlob(nsDOMFileBase* aBlob, - BlobConstructorParams* aOutParams) -{ - BlobConstructorParams resultParams; - - if (!(aBlob->IsMemoryBacked() || aBlob->GetSubBlobs()) && - (aBlob->IsSizeUnknown() || aBlob->IsDateUnknown())) { - // We don't want to call GetSize or GetLastModifiedDate - // yet since that may stat a file on the main thread - // here. Instead we'll learn the size lazily from the - // other process. - resultParams = MysteryBlobConstructorParams(); - } - else { - BlobOrFileConstructorParams params; - - nsString contentType; - nsresult rv = aBlob->GetType(contentType); - NS_ENSURE_SUCCESS(rv, false); - - uint64_t length; - rv = aBlob->GetSize(&length); - NS_ENSURE_SUCCESS(rv, false); - - nsCOMPtr<nsIDOMFile> file; - static_cast<nsIDOMBlob*>(aBlob)->QueryInterface(NS_GET_IID(nsIDOMFile), - (void**)getter_AddRefs(file)); - if (file) { - FileBlobConstructorParams fileParams; - - rv = file->GetName(fileParams.name()); - NS_ENSURE_SUCCESS(rv, false); - - rv = file->GetMozLastModifiedDate(&fileParams.modDate()); - NS_ENSURE_SUCCESS(rv, false); - - fileParams.contentType() = contentType; - fileParams.length() = length; - - params = fileParams; - } else { - NormalBlobConstructorParams blobParams; - blobParams.contentType() = contentType; - blobParams.length() = length; - params = blobParams; - } - - MOZ_ASSERT(!(aBlob->IsMemoryBacked() && - aBlob->GetSubBlobs()), "Can't be both!"); - - if (aBlob->IsMemoryBacked()) { - const nsDOMMemoryFile* memoryBlob = - static_cast<const nsDOMMemoryFile*>(aBlob); - - InfallibleTArray<uint8_t> data; - data.SetLength(memoryBlob->GetLength()); - memcpy(data.Elements(), memoryBlob->GetData(), memoryBlob->GetLength()); - - MemoryBlobOrFileConstructorParams memoryParams(params, data); - resultParams = memoryParams; - } - else if (aBlob->GetSubBlobs()) { - MultipartBlobOrFileConstructorParams multipartParams(params); - resultParams = multipartParams; - } - else { - resultParams = BlobConstructorNoMultipartParams(params); - } - } - - *aOutParams = resultParams; - - return true; -} - BlobChild* ContentChild::GetOrCreateActorForBlob(nsIDOMBlob* aBlob) { NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); NS_ASSERTION(aBlob, "Null pointer!"); // XXX This is only safe so long as all blob implementations in our tree // inherit nsDOMFileBase. If that ever changes then this will need to grow // a real interface or something. - nsDOMFileBase* blob = static_cast<nsDOMFileBase*>(aBlob); + const nsDOMFileBase* blob = static_cast<nsDOMFileBase*>(aBlob); // All blobs shared between processes must be immutable. nsCOMPtr<nsIMutable> mutableBlob = do_QueryInterface(aBlob); if (!mutableBlob || NS_FAILED(mutableBlob->SetMutable(false))) { NS_WARNING("Failed to make blob immutable!"); return nullptr; } @@ -672,44 +595,73 @@ ContentChild::GetOrCreateActorForBlob(ns if (remoteBlob) { BlobChild* actor = static_cast<BlobChild*>(static_cast<PBlobChild*>(remoteBlob->GetPBlob())); NS_ASSERTION(actor, "Null actor?!"); return actor; } - BlobConstructorParams params; - if (!GetParamsForBlob(blob, ¶ms)) { - return nullptr; + ParentBlobConstructorParams params; + + if (blob->IsSizeUnknown() || blob->IsDateUnknown()) { + // We don't want to call GetSize or GetLastModifiedDate + // yet since that may stat a file on the main thread + // here. Instead we'll learn the size lazily from the + // other process. + params.blobParams() = MysteryBlobConstructorParams(); + params.optionalInputStreamParams() = void_t(); } + else { + nsString contentType; + nsresult rv = aBlob->GetType(contentType); + NS_ENSURE_SUCCESS(rv, nullptr); + + uint64_t length; + rv = aBlob->GetSize(&length); + NS_ENSURE_SUCCESS(rv, nullptr); + + nsCOMPtr<nsIInputStream> stream; + rv = aBlob->GetInternalStream(getter_AddRefs(stream)); + NS_ENSURE_SUCCESS(rv, nullptr); + + InputStreamParams inputStreamParams; + SerializeInputStream(stream, inputStreamParams); + + params.optionalInputStreamParams() = inputStreamParams; + + nsCOMPtr<nsIDOMFile> file = do_QueryInterface(aBlob); + if (file) { + FileBlobConstructorParams fileParams; + + rv = file->GetName(fileParams.name()); + NS_ENSURE_SUCCESS(rv, nullptr); + + rv = file->GetMozLastModifiedDate(&fileParams.modDate()); + NS_ENSURE_SUCCESS(rv, nullptr); + + fileParams.contentType() = contentType; + fileParams.length() = length; + + params.blobParams() = fileParams; + } else { + NormalBlobConstructorParams blobParams; + blobParams.contentType() = contentType; + blobParams.length() = length; + params.blobParams() = blobParams; + } + } BlobChild* actor = BlobChild::Create(aBlob); NS_ENSURE_TRUE(actor, nullptr); - SendPBlobConstructor(actor, params); - - actor->SetManager(this); - - if (const nsTArray<nsCOMPtr<nsIDOMBlob> >* subBlobs = blob->GetSubBlobs()) { - for (uint32_t i = 0; i < subBlobs->Length(); ++i) { - BlobConstructorParams subParams; - nsDOMFileBase* subBlob = - static_cast<nsDOMFileBase*>(subBlobs->ElementAt(i).get()); - if (!GetParamsForBlob(subBlob, &subParams)) { + if (!SendPBlobConstructor(actor, params)) { return nullptr; } - BlobChild* subActor = BlobChild::Create(aBlob); - actor->SendPBlobConstructor(subActor, subParams); - - subActor->SetManager(actor); - } - } - return actor; } PCrashReporterChild* ContentChild::AllocPCrashReporter(const mozilla::dom::NativeThreadId& id, const uint32_t& processType) { #ifdef MOZ_CRASHREPORTER @@ -1180,9 +1132,9 @@ ContentChild::RecvFileSystemUpdate(const unused << aName; unused << aState; unused << aMountGeneration; #endif return true; } } // namespace dom -} // namespace mozilla +} // namespace mozilla \ No newline at end of file
--- a/dom/ipc/ContentChild.h +++ b/dom/ipc/ContentChild.h @@ -12,17 +12,16 @@ #include "mozilla/dom/TabContext.h" #include "mozilla/dom/ipc/Blob.h" #include "nsTArray.h" #include "nsIConsoleListener.h" struct ChromePackage; class nsIDOMBlob; -class nsDOMFileBase; class nsIObserver; struct ResourceMapping; struct OverrideMapping; namespace mozilla { namespace ipc { class OptionalURIParams; @@ -198,18 +197,16 @@ public: // cache the value nsString &GetIndexedDBPath(); uint64_t GetID() { return mID; } bool IsForApp() { return mIsForApp; } bool IsForBrowser() { return mIsForBrowser; } - bool GetParamsForBlob(nsDOMFileBase* aBlob, - BlobConstructorParams* aOutParams); BlobChild* GetOrCreateActorForBlob(nsIDOMBlob* aBlob); private: virtual void ActorDestroy(ActorDestroyReason why) MOZ_OVERRIDE; virtual void ProcessingError(Result what) MOZ_OVERRIDE; /**
--- a/dom/ipc/ContentParent.cpp +++ b/dom/ipc/ContentParent.cpp @@ -1455,168 +1455,103 @@ ContentParent::DeallocPDeviceStorageRequ DeviceStorageRequestParent *parent = static_cast<DeviceStorageRequestParent*>(doomed); NS_RELEASE(parent); return true; } PBlobParent* ContentParent::AllocPBlob(const BlobConstructorParams& aParams) { - BlobParent* actor = BlobParent::Create(aParams); - actor->SetManager(this); - return actor; + return BlobParent::Create(aParams); } bool ContentParent::DeallocPBlob(PBlobParent* aActor) { delete aActor; return true; } -bool -ContentParent::GetParamsForBlob(nsDOMFileBase* aBlob, - BlobConstructorParams* aOutParams) -{ - BlobConstructorParams resultParams; - - if (!(aBlob->IsMemoryBacked() || aBlob->GetSubBlobs()) && - (aBlob->IsSizeUnknown() || aBlob->IsDateUnknown())) { - // We don't want to call GetSize or GetLastModifiedDate - // yet since that may stat a file on the main thread