Skip to content

Audio position resets when using (mobile) notification controls, Media Session API vs .play()/.pause(). #1262

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
bikubi opened this issue Dec 20, 2019 · 8 comments

Comments

@bikubi
Copy link
Contributor

bikubi commented Dec 20, 2019

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:

  • the Android/Chrome notification controls fire 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"
  • This is all, of course, pre Media Session API

Any insight welcome!

@bikubi
Copy link
Contributor Author

bikubi commented Jan 21, 2020

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.

@bikubi
Copy link
Contributor Author

bikubi commented Jan 21, 2020

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.

@bikubi bikubi changed the title Audio position resets when using (mobile) notification controls & .play()/.pause(). Audio position resets when using (mobile) notification controls, Media Session API vs .play()/.pause(). Jan 27, 2020
@bikubi
Copy link
Contributor Author

bikubi commented Jan 27, 2020

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.

@bikubi
Copy link
Contributor Author

bikubi commented Feb 12, 2020

...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._paused
  • onended - if paused then assume external effect (buffer underrun), not user interaction → .stop()
  • onplaying - unset sound._paused

@satyrius
Copy link

satyrius commented Oct 3, 2022

Hello @bikubi 👋 Could you please share a full workaround example with us, while we all pray for #1530 to fix the problem eventually. It's not clear how you fixed it with the snippet above and setActionHandler

@satyrius
Copy link

satyrius commented Oct 3, 2022

After trials and errors this recipe worked #1175 (comment)

@bikubi
Copy link
Contributor Author

bikubi commented Oct 5, 2022

@satyrius if you're still interested i could throw a MWE together next week (on vacation r/n)

@satyrius
Copy link

satyrius commented Oct 5, 2022

@bikubi It would be great to look at your solution. Ofc no rush with it.

Some observation. I managed to make it work with setActionHandler on play/pause event. When they are triggered I explicitly call howl's play/pause. But this handlers stop working occasionally. Hard refresh of the page help the situation. And also iOS Notification Center play button is not working, but pause do.

Here's a snippet from my react component.

import React, { useState, useEffect } from 'react'
import { getStorage, ref, getDownloadURL } from 'firebase/storage'
import { Howl } from 'howler'

function Player({ file }) {
  const [position, setPosition] = useState(0)
  const [isLoaded, setLoaded] = useState(false)
  const [isPlaying, setPlaying] = useState(false)
  const [audio, setAudio] = useState()

  useEffect(() => {
    if (file)
      getDownloadURL(ref(getStorage(), file))
        .then((url) => {
          const howl = new Howl({
            src: [url],
            html5: true,
            onplay: () => {
              setPlaying(true)
              showCurrentPosition()
            },
            onpause: () => {
              setPlaying(false)
            },
          })

          const showCurrentPosition = () => {
            const sec = Math.floor(howl.seek())
            setPosition(sec)
            if (howl.playing()) {
              setTimeout(showCurrentPosition, 200)
            }
          }

          setAudio(howl)
          setLoaded(true)
        })
        .catch((error) => {
          setAudio(null)
          setLoaded(false)
        })
  }, [file])

  // Sync Media Session with Howl
  // https://github.com/goldfire/howler.js/issues/1175
  useEffect(() => {
    if (audio && navigator && navigator.mediaSession) {
      navigator.mediaSession.setActionHandler('play', () => audio.play())
      navigator.mediaSession.setActionHandler('pause', () => audio.pause())
    }
  }, [audio])

  // rendering...
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants