Best practice Q: canceling play() while loading

1,390 views
Skip to first unread message

Victoria Kirst

unread,
May 21, 2018, 5:50:31 PM5/21/18
to medi...@chromium.org
Hey everyone!
(and hello to some old friends!)

tl;dr: 
Q: If you want to change the src of a media tag before the previous play()'s Promise has resolved, is it best practice to simply catch the exception?

Full background:
I am implementing something that plays ambient audio for different "scenes." Here's a simplified demo:

My demo works fine with a fast enough internet connection, but if you:
1. Open Chrome DevTools -> Network tab -> check disable cache and simulate Fast 3G (screenshot)
2. Click the "play" button
3. Then click "next scene!" a few times quickly (screenshot)

There's an (expected) exception thrown: "Uncaught (in promise) DOMException: The play() request was interrupted by a new load request. https://goo.gl/LdLk22"

This makes sense, because I'm changing the src and calling play() before the previous play() Promise has resolved, and I believe what's happening is that in step 3 of load(), all pending play Promises are rejected.

The DevRel article linked suggests waiting for the previous play() Promise to be resolved, but that doesn't apply to my situation because I actually *don't* want to load or play any part of e.g. Scene 2's audio if the user has clicked into Scene 3 before Scene 2's audio loaded.

So what I've done instead is this:

  audioElement.play().catch((e) => {
    if (e.code === DOMException.ABORT_ERR) {
      console.log('previous play() was aborted, which is fine');
      return;
    }
    // Unexpected exception - rethrow.
    throw e;
  });

i.e. I just catch the exception and ignore it...

Is that best practice for this scenario, or is there a better way to effectively cancel the previous load? Or am I approaching this in the wrong way?

Thanks everyone!
Victoria

Dale Curtis

unread,
May 21, 2018, 6:57:45 PM5/21/18
to Victoria Kirst, medi...@chromium.org, Mounir Lamouri, François Beaufort
I don't know of a better way; Mounir, Francois?

You might be able to scope the ignore a bit more if you use bind currentSoundIndex into the promise, but that's about it. I don't know how to do that with => syntax either, but here's the old way:

  setSoundAtIndex(nextIndex);
  function ignoreErrorsForPastSounds(idx, e) {
    if (idx != currentSoundIndex)
      return;
    throw e;
  }
  
  currentSoundIndex = nextIndex;  
  audioElement.play().catch(ignoreErrorsForPastSounds.bind(null, currentSoundIndex));

You can use some sort of token system instead if you need the ability to go back and forth.

- dale

--
You received this message because you are subscribed to the Google Groups "media-dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to media-dev+...@chromium.org.
To post to this group, send email to medi...@chromium.org.
To view this discussion on the web visit https://groups.google.com/a/chromium.org/d/msgid/media-dev/CANLdJ-fimkbz9R5hKQH9fYQFKOU%2BhSj%3DS8XGDQXuZDGyotr7wg%40mail.gmail.com.

Mounir Lamouri

unread,
May 22, 2018, 6:09:12 PM5/22/18
to Dale Curtis, Victoria Kirst, medi...@chromium.org, François Beaufort
Victoria, I think the solution you have is fine. The solution Dale suggested is more complex but would have the benefit of firing abort errors when it's not due to another playback starting (for example, if pause() is called). The gist stays the same though.

I am not aware of any better way to deal with that kind of errors. They are mostly here to explain why the play promise can't be resolved.

-- Mounir

Victoria Kirst

unread,
May 22, 2018, 6:24:56 PM5/22/18
to Mounir Lamouri, Dale Curtis, medi...@chromium.org, François Beaufort
Thanks so much, Dale and Mounir!

Actually, I have a related question for an even simpler scenario: Let's say I'm just trying to toggle play() and pause() for a single audio file.

- Naive attempt 1: Toggle play/pause without paying attention to the play() Promise, and get an exception as expected (code)
- Naive attempt 2: Toggle play/pause and overwrite the previous play() Promise each time (code) -- this is buggy because if you're on a slow network and rapidly click "play" -> "pause" -> "play" --> then the state should be playing, but the original play() Promise is still sticking around, so it resolves and pauses as soon as the audio loads (gif of bug)

My approach: 
Chain all the play() Promises together: https://codepen.io/bee-arcade/pen/YLMwpQ

Simplified it looks roughly like this:

const audioElement = new Audio();
playButtonElement.addEventListener('click', togglePlayState);
let playPromise = Promise.resolve();
function togglePlayState() {
  isPlaying = !isPlaying;  
  if (isPlaying) {
    playPromise = playPromise.then(() => {
      return audioElement.play();
    });
  } else {
    playPromise = playPromise.then(() => {
      audioElement.pause();
    });
  }
}

This means on a slow network and you toggle play->pause->play->pause->etc, then when the load finally completes, it rapidly toggles play/pause/play/pause... etc until it gets in the correct state :) 

Gif of this behavior: https://imgur.com/a/6TD5Tys

Similar question as before: Is this best practice, or is there another way of doing this? (I think ideally Promises would be cancelable, but alas...)

Victoria

Dale Curtis

unread,
May 22, 2018, 6:29:05 PM5/22/18
to Victoria Kirst, Mounir Lamouri, medi...@chromium.org, François Beaufort
Again I don't know of one. Using approach #1 plus the code I suggested to ignore messages from past attempts is my best suggestion.

- dale

Ehsan Bayranvand

unread,
May 9, 2022, 12:21:10 PM5/9/22
to media-dev, Dale Curtis, Mounir Lamouri, medi...@chromium.org, François Beaufort, Victoria Kirst
you could put it in a setTimeout with little delay like 200ms, I know it is not clean but it works for me...and hope works for you

guest271314

unread,
May 9, 2022, 6:58:47 PM5/9/22
to media-dev, Ehsan Bayranvand, Dale Curtis, Mounir Lamouri, medi...@chromium.org, François Beaufort, Victoria Kirst
You can 

A. fetch and cache all of the media first, either using fetch() with Promise.all() (or BackgroundFetch) and as one or more ArrayBuffer or WebAssembly.Memory to avoid fetching media during the playback; e.g., https://github.com/guest271314/MediaFragmentRecorder/blob/webrtc-replacetrack/MediaFragmentRecorder.html, et al. branches; e.g., 
3. Use Web Audio API with previously stored ArrayBuffer(s) as source buffers;
4. Use MediaSource with segments of media;

For all options A. is applicable in this use case, where the audio playback is not expected to be dynamic or real-time.

Note, HTMLMediaElement has paused property, which can be substituted for 

 isPlaying = !isPlaying;  
Reply all
Reply to author
Forward
0 new messages