diff --git a/README.md b/README.md index 4a4766f0..2bb6c3b5 100644 --- a/README.md +++ b/README.md @@ -378,9 +378,11 @@ Automatically attempts to enable audio on mobile (iOS, Android, etc) devices and Each HTML5 Audio object must be unlocked individually, so we keep a global pool of unlocked nodes to share between all `Howl` instances. This pool gets created on the first user interaction and is set to the size of this property. #### autoSuspend `Boolean` `true` Automatically suspends the Web Audio AudioContext after 30 seconds of inactivity to decrease processing and energy usage. Automatically resumes upon new playback. Set this property to `false` to disable this behavior. -#### ctx `Boolean` *`Web Audio Only`* +#### eagerPlayback `Boolean` `false` +When enabled, allows playback to begin before the browser has estimated that enough data has loaded in order to play the sound to its end, without having to stop and buffer for more content. +#### ctx `AudioContext` `null` *`Web Audio Only`* Exposes the `AudioContext` with Web Audio API. -#### masterGain `Boolean` *`Web Audio Only`* +#### masterGain `GainNode` `null` *`Web Audio Only`* Exposes the master `GainNode` with Web Audio API. This can be useful for writing plugins or advanced usage. diff --git a/src/howler.core.js b/src/howler.core.js index aa60cdbe..442a39ca 100644 --- a/src/howler.core.js +++ b/src/howler.core.js @@ -51,6 +51,7 @@ self.usingWebAudio = true; self.autoSuspend = true; self.ctx = null; + self.eagerPlayback = false; // Set to false to disable the auto audio unlocker. self.autoUnlock = true; @@ -63,8 +64,8 @@ /** * Get/set the global volume for all sounds. - * @param {Float} vol Volume from 0.0 to 1.0. - * @return {Howler/Float} Returns self or current volume. + * @param {float} vol Volume from 0.0 to 1.0. + * @return {Howler/float} Returns self or current volume. */ volume: function(vol) { var self = this || Howler; @@ -539,6 +540,22 @@ self._resumeAfterSuspend = true; } + return self; + }, + + /** + * Use `canplay` event for determining when playback can begin. In contrast with the `canplaythrough` event, + * `canplay` is fired when enough data has been loaded to begin playing the media, but not necessarily enough to + * play without stopping and buffering additional data. + * @return {Howler} + */ + _enableEagerPlayback: function() { + var self = this; + + if (self._canPlayEvent === 'canplaythrough') { + self._canPlayEvent = 'canplay'; + } + return self; } }; @@ -623,6 +640,10 @@ // Web Audio or HTML5 Audio? self._webAudio = Howler.usingWebAudio && !self._html5; + if (Howler.eagerPlayback) { + Howler._enableEagerPlayback(); + } + // Automatically try to enable audio. if (typeof Howler.ctx !== 'undefined' && Howler.ctx && Howler.autoUnlock) { Howler._unlockAudio(); @@ -897,7 +918,11 @@ } else { // Fire this when the sound is ready to play to begin HTML5 Audio playback. var playHtml5 = function() { - node.currentTime = seek; + // When `eagerPlayback` is enabled, setting `currentTime` to the same value prevents the + // play promise from ever resolving in Chromium-based browsers. + if (node.currentTime !== seek) { + node.currentTime = seek; + } node.muted = sound._muted || self._muted || Howler._muted || node.muted; node.volume = sound._volume * Howler.volume(); node.playbackRate = sound._rate; @@ -974,7 +999,7 @@ node.load(); } - // Play immediately if ready, or wait for the 'canplaythrough'e vent. + // Play immediately if ready, or wait for the '_canPlayEvent' event. var loadedNoReadyState = (window && window.ejecta) || (!node.readyState && Howler._navigator.isCocoonJS); if (node.readyState >= 3 || loadedNoReadyState) { playHtml5(); @@ -984,7 +1009,7 @@ var listener = function() { self._state = 'loaded'; - + // Begin playback. playHtml5(); @@ -2260,7 +2285,7 @@ self._errorFn = self._errorListener.bind(self); self._node.addEventListener('error', self._errorFn, false); - // Listen for 'canplaythrough' event to let us know the sound is ready. + // Listen for '_canPlayEvent' event to let us know the sound is ready. self._loadFn = self._loadListener.bind(self); self._node.addEventListener(Howler._canPlayEvent, self._loadFn, false); diff --git a/tests/core.html5audio.html b/tests/core.html5audio.html index ecf3ae67..bc444ad3 100644 --- a/tests/core.html5audio.html +++ b/tests/core.html5audio.html @@ -12,6 +12,7 @@ + - \ No newline at end of file + diff --git a/tests/core.webaudio.html b/tests/core.webaudio.html index 7c16bae6..1e42da8b 100644 --- a/tests/core.webaudio.html +++ b/tests/core.webaudio.html @@ -11,6 +11,7 @@ + - \ No newline at end of file + diff --git a/tests/css/styles.css b/tests/css/styles.css index 501b568e..9e31c279 100644 --- a/tests/css/styles.css +++ b/tests/css/styles.css @@ -1,6 +1,6 @@ html { width: 100%; - height: 100%; + height: 100%; overflow: hidden; padding: 0; margin: 0; @@ -89,8 +89,46 @@ body { align-items: center; } +#globalOptionsHeader { + font-size: 3vw; + margin: 0 0 1rem; +} + +#globalOptions { + display: flex; + flex-wrap: wrap; + justify-content: center; + width: 92%; + margin: 0 2rem 3rem; +} + +.option { + display: -webkit-box; + display: -ms-flexbox; + display: -webkit-flex; + display: flex; + -webkit-justify-content: space-between; + -webkit-box-pack: justify; + -ms-flex-pack: distribute; + justify-content: space-between; + -webkit-box-align: center; + -webkit-align-items: center; + -ms-flex-align: center; + align-items: center; + margin: 0 2rem 1rem 0; + font-size: 2vw; +} + +.option > * { + margin-bottom: 0; +} + +.option > label { + margin-right: 0.5rem; +} + @media screen and (max-height: 400px) { .button { padding: 5px; } -} \ No newline at end of file +} diff --git a/tests/index.html b/tests/index.html index a30b0201..2a326011 100644 --- a/tests/index.html +++ b/tests/index.html @@ -6,24 +6,70 @@ -
- - - -
+
+
Global Options:
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + +
- + document.getElementById('webaudio').onclick = function() { + navigateToTest('core.webaudio.html'); + }; + + document.getElementById('html5').onclick = function() { + navigateToTest('core.html5audio.html'); + }; + + document.getElementById('spatial').onclick = function() { + navigateToTest('spatial.html'); + }; + - \ No newline at end of file + diff --git a/tests/js/core.html5audio.js b/tests/js/core.html5audio.js index cd546ef7..47da99e0 100644 --- a/tests/js/core.html5audio.js +++ b/tests/js/core.html5audio.js @@ -30,11 +30,13 @@ sound1.once('load', function() { // Define the tests to run. var id; var tests = [ - function(fn) { - id = sound1.play(); + function(fn) { + sound1.once('play', function() { + label.innerHTML = 'PLAYING'; + setTimeout(fn, 2000); + }); - label.innerHTML = 'PLAYING'; - setTimeout(fn, 2000); + id = sound1.play(); }, function(fn) { @@ -265,4 +267,4 @@ var chain = function(i) { start.addEventListener('click', function() { tests[0](chain(1)); start.style.display = 'none'; -}, false); \ No newline at end of file +}, false); diff --git a/tests/js/core.webaudio.js b/tests/js/core.webaudio.js index f6690310..4b673df4 100644 --- a/tests/js/core.webaudio.js +++ b/tests/js/core.webaudio.js @@ -33,7 +33,7 @@ var tests = [ label.innerHTML = 'PLAYING'; setTimeout(fn, 2000); }); - + id = sound1.play(); }, @@ -269,4 +269,4 @@ if (Howler.usingWebAudio) { }, false); } else { window.location = 'core.html5audio.html'; -} \ No newline at end of file +} diff --git a/tests/js/parseGlobalOptions.js b/tests/js/parseGlobalOptions.js new file mode 100644 index 00000000..d799a939 --- /dev/null +++ b/tests/js/parseGlobalOptions.js @@ -0,0 +1,33 @@ +// Set global options from query params +function parseValueFromEntry(entry) { + var key = entry[0]; + var value = entry[1]; + var parsedValue = value; + + if (value.toLowerCase() === 'true') { + parsedValue = true; + } else if (value.toLowerCase() === 'false') { + parsedValue = false; + } else if (!isNaN(value)) { + parsedValue = parseFloat(value); + } + + return [key, parsedValue]; +} + +function setGlobalOptions(entry) { + var key = entry[0]; + var value = entry[1]; + + if (Howler.hasOwnProperty(key)) { + Howler[key] = value; + } +} + +window.location.search + .slice(1) + .split('&') + .filter(function(entry) { return entry[0]; }) + .map(function(pair) { return pair.split('='); }) + .map(parseValueFromEntry) + .forEach(setGlobalOptions); diff --git a/tests/js/spatial.js b/tests/js/spatial.js index a7aa810d..fa330d31 100644 --- a/tests/js/spatial.js +++ b/tests/js/spatial.js @@ -33,7 +33,7 @@ var tests = [ label.innerHTML = 'PLAYING'; setTimeout(fn, 2000); }); - + id = sound1.play(); }, @@ -116,4 +116,4 @@ if (Howler.usingWebAudio) { }, false); } else { window.location = 'core.html5audio.html'; -} \ No newline at end of file +} diff --git a/tests/spatial.html b/tests/spatial.html index a9776d75..87011717 100644 --- a/tests/spatial.html +++ b/tests/spatial.html @@ -12,6 +12,7 @@ + - \ No newline at end of file +