Bug 255024 - Playing Video with Intersection Observer results in "Unhandled Promise Rejection: AbortError: The operation was aborted."
Summary: Playing Video with Intersection Observer results in "Unhandled Promise Reject...
Status: RESOLVED INVALID
Alias: None
Product: WebKit
Classification: Unclassified
Component: Media (show other bugs)
Version: Safari 16
Hardware: All iOS 16
: P2 Critical
Assignee: Nobody
URL:
Keywords:
Depends on:
Blocks:
 
Reported: 2023-04-05 02:29 PDT by Ben Frain
Modified: 2023-04-11 07:33 PDT (History)
3 users (show)

See Also:


Attachments

Note You need to log in before you can comment on or make changes to this bug.
Description Ben Frain 2023-04-05 02:29:29 PDT
This works without problem in Chrome and Firefox. Fails on Safari up to, and including Release 166 (Safari 16.4, WebKit 18616.1.6.11). 

The error in the console is "Unhandled Promise Rejection: AbortError: The operation was aborted."

Test page: https://benfrain.com/playground/video-stop-start/index.html

What should happen is that the videos pause as they are scrolled past and play when they are scrolled into view. This mechanism is achieved with this code:


```
function playPauseVideo() {
  let videos = document.querySelectorAll("video");
  videos.forEach((video) => {
    // We can only control playback without insteraction if video is mute
    video.muted = true;
    // Play is a promise so we need to check we have it
    let playPromise = video.play();
    if (playPromise !== undefined) {
      playPromise.then((_) => {
        let observer = new IntersectionObserver(
          (entries) => {
            entries.forEach((entry) => {
              if (entry.intersectionRatio !== 1 && !video.paused) {
                video.pause();
              } else if (video.paused) {
                video.play();
              }
            });
          },
          { threshold: 0.2 }
        );
        observer.observe(video);
      });
    }
  });
}
```

I am unsure if this is a bug or a missing piece of requisite (undocumented) code that only Safari needs?
Comment 1 Sam Sneddon [:gsnedders] 2023-04-05 05:46:27 PDT
AFAIK, this is related to transitive user activation—I believe Firefox and Chrome allow video play to begin based on transitive user activation, whereas I think we're stricter about this.

In Firefox and Chrome, the scroll event that triggers the intersection observer will allow a play for N seconds, thus by the time the intersection observer fires a play is allowed.

In Safari, I believe we only allow a play directly from the scroll event, which thus means going via an intersection observer doesn't work.

That said, I'm sure we tried to change this recently and then hit web compat problems by matching others.
Comment 2 Ben Frain 2023-04-05 06:55:21 PDT
Hi Sam, thanks for the prompt response. 

Given what you said, and this seeming like a pretty typical use case around playing media, what is the current 'Safari' way to achieve the same goal?

Also, could you clarify; do you mean there is a chance Safari may eventually align itself with the other browsers in its execution, or that seems unlikely?
Comment 3 Marcos Caceres 2023-04-10 22:13:15 PDT
I don't think the problem is to do with user activation. I think it may be that playPauseVideo() is trying to play() the videos before they have loaded. 

Ben, can you try: 
window.onload = () => playPauseVideo();
Comment 4 Marcos Caceres 2023-04-10 22:48:49 PDT
(In reply to Marcos Caceres from comment #3)
> I don't think the problem is to do with user activation. I think it may be
> that playPauseVideo() is trying to play() the videos before they have
> loaded. 
> 
> Ben, can you try: 
> window.onload = () => playPauseVideo();


Here is an alternative that checks if the video is not ready:

function videoObsever(video) {
  return async (entries, observer) => {
    // guard against playing too early
    if (video.readyState < video.HAVE_CURRENT_DATA) {
      console.warn("video not ready yet");
      return;
    }

    for (entry of entries) {
      if (!entry.isIntersecting) {
        video.pause();
        continue;
      }
      await video.play();
    }
  };
}

document.querySelectorAll("video").forEach((video) => {
  video.pause();
  new IntersectionObserver(videoObsever(video), {
    threshold: 0.2,
  }).observe(video);
});
Comment 5 Ben Frain 2023-04-11 00:35:47 PDT
Hi Marcos, thanks for taking a look. Your second comment mostly works but does not play the video(s) that is in view when the page is first loaded.

Here it is an example: https://benfrain.com/playground/video-stop-start/indexAlt.html
Comment 6 Marcos Caceres 2023-04-11 04:10:56 PDT
(In reply to Ben Frain from comment #5)
> Hi Marcos, thanks for taking a look. Your second comment mostly works but
> does not play the video(s) that is in view when the page is first loaded.
> 
> Here it is an example:
> https://benfrain.com/playground/video-stop-start/indexAlt.html

Apologies, Ben. I was only checking if the AbortError was related to user activation and didn’t provide a complete solution. It should be possible to modify the code a bit to play the videos that are intersecting. 

Can you give that a go? Otherwise I’ll try to provide you with an updated solution tomorrow. If we can get it working I think we can close this issue 🤞
Comment 7 Marcos Caceres 2023-04-11 05:16:25 PDT
(In reply to Marcos Caceres from comment #6)
> (In reply to Ben Frain from comment #5)
> > Hi Marcos, thanks for taking a look. Your second comment mostly works but
> > does not play the video(s) that is in view when the page is first loaded.
> > 
> > Here it is an example:
> > https://benfrain.com/playground/video-stop-start/indexAlt.html
> 
> Apologies, Ben. I was only checking if the AbortError was related to user
> activation and didn’t provide a complete solution. It should be possible to
> modify the code a bit to play the videos that are intersecting. 
> 
> Can you give that a go? Otherwise I’ll try to provide you with an updated
> solution tomorrow. If we can get it working I think we can close this issue
> 🤞

Couldn't help myself... I think this works:

```JS
function videoObsever(video) {
  return async (entries, observer) => {
    for (entry of entries) {
      if (!entry.isIntersecting) {
        video.pause();
        console.log("paused", video);
        continue;
      }
      // Guard against playing too early
      if (video.readyState < video.HAVE_CURRENT_DATA) {
        console.warn("video not ready yet", video);
        await new Promise((r) => {
          video.addEventListener("canplay", r, { once: true });
        });
        console.log("video ready", video);
      }
      await video.play();
      console.log("playing", video);
    }
  };
}
document.querySelectorAll("video").forEach((video) => {
  video.pause();
  new IntersectionObserver(videoObsever(video), {
    threshold: 0.2,
  }).observe(video);
});
```

In any case... Ben, is it ok if we close this issue? I think we've shown that Webkit is working as expected, right?
Comment 8 Ben Frain 2023-04-11 06:27:50 PDT
Thanks Marcos,

I guess my question is, what about the original code was problematic? Is there a good reason this should not work in WebKit when it works in Firefox and Chromium browsers. 

If you are happy that WebKit is behaving here as you would hope and expect, by all means close. 👍
Comment 9 Marcos Caceres 2023-04-11 07:33:11 PDT
(In reply to Ben Frain from comment #8)
> Thanks Marcos,
> 
> I guess my question is, what about the original code was problematic? Is
> there a good reason this should not work in WebKit when it works in Firefox
> and Chromium browsers. 

Testing locally with Firefox I can see it sometimes also hits the situation where the video is not available. It may be thay the video was cached by those browsers so it was always ready to play?

I think you would eventually need to add similar logic to what I added for when the  video’s ready state doesn’t yet allow playing the video. You could test for that by maybe sending a bad video or delaying the response from the server. 

> If you are happy that WebKit is behaving here as you would hope and expect,
> by all means close. 👍

Cheers and best of success with what you are building!