Or, more generally, does Howler handle mobile OS audio controls?
On mobile devices, when you play HTML5 Audio, the OS let's you control it. (Android: notification drawer, per-app + headset remote control, somewhat globally; iOS: "Control Center", globally).
My web app has regular (browser) control buttons, too. Both control schemes (let's call them "OS" and "browser") should work seamlessly & interchangeably, IMO. But, they only work as long as i stick to one scheme exclusively (pause/unpause by browser, pause/unpause by OS) - as soon as I mix (play by browser, pause by OS, unpause by browser), the audio position is reset to zero.
As far as I can tell, this happens somewhere in play(), but I could not pin it down exactly.
I have made a Pen to reproduce the issue, with instructions. Tested on Android 9 / Chrome 78.
Note that this does not occur on the Music Player example, where you have to essentially pause twice (play by browser, pause by OS, _pause again_ by browser, then play by browser).
Background:
play and pause events on the Audio node, but just by looking at the event AFAIK there is no way to tell that they came "from the OS"Any insight welcome!
Update: mobile-like media controls will land soon in Chrome (you can enable chrome://flags/#global-media-controls, see also ars technica article).
I just reproduced the issue on v81 on desktop.
I found a workaround:
The problem is, that Howler assumes it has exclusive control over the Audio node, so it relies on parameters on the sounds objects to manage state. When pause/play is triggered externally (e.g. via media control), the _paused property is not updated, and Howler doesn't take the node into consideration for re-use, and spawns a new one.
So I listen for pause/play events on the Audio node, and correct the _paused property manually.
Furthermore, I have to also set _seek to the node's currentTime.
Here's a pen of an example implementation. Gist:
var howl = new Howl({
html5: true,
src: ['https://....mp3']
})
var node = howl._sounds[0]._node
node.onpause = function () {
// find sound by node
var s, sound
for (s = 0; s < howl._sounds.length; s++) {
if (howl._sounds[s]._node === this) {
sound = howl._sounds[s]
}
}
if (!sound) return
sound._paused = true
/* don't do the next thing if this is a live stream!
* otherwise howler will fire an end event */
sound._seek = this.currentTime
}.bind(node)
Please note that the examples of the older pen (linked above) might not work today-ish, the MP3 server is down.
I'd be happy to provide a PR, but I lack experience with Howler and its intricacies.
FYI, to whom it may concern in the future, the workaround above allowed me to integrate Howler somewhat successfully with the Media Session API (setActionHandler with play, pause/stop, as well as seekforward/seekbackward.
...and I've managed to make it work with pre-MediaSession (older browsers, iOS Control Center) controls AND live streams. The solution above apparently worked only with fixed-length files.
Gist:
onpause - assume user interaction, set sound._pausedonended - if paused then assume external effect (buffer underrun), not user interaction → .stop()onplaying - unset sound._paused
Most helpful comment
...and I've managed to make it work with pre-MediaSession (older browsers, iOS Control Center) controls AND live streams. The solution above apparently worked only with fixed-length files.
Gist:
onpause- assume user interaction, setsound._pausedonended- ifpausedthen assume external effect (buffer underrun), not user interaction →.stop()onplaying- unsetsound._paused