Giter Club home page Giter Club logo

Comments (19)

cpearce avatar cpearce commented on August 19, 2024

Also, does canAutoplay() resolves successfully mean that the media resource is of a format supported by the UA, and is it a valid/playable resource? That is, should the promise resolve be delayed until the "canplay" event has been fired?

from autoplay.

jernoble avatar jernoble commented on August 19, 2024

@cpearce said:

Why does this API needs to be async? It would be simpler if it was synchronous. What case does making this API async cover that a boolean readonly attribute on HTMLMediaElement (or a function returning a boolean) couldn't cover?

Without putting too many words into his mouth, at TPAC @mounirlamouri made the argument that canAutoplay() should return a Promise so that UAs could return all the normal Error codes like NotAllowedError and InvalidStateError without having to make up some new return value. IIRC, the rough spec was something like:

if readyState < HAVE_METADATA
   reject(InvalidStateError)
else if !isAllowedToPlay
   reject(NotAllowedError)
else
   resolve()

from autoplay.

jernoble avatar jernoble commented on August 19, 2024

To continue my thought, and to put more words into @mounirlamouri 's mouth, the incentive for making canAutoplay() promise-based was strictly about the return type, and not about async-vs-sync.

from autoplay.

cpearce avatar cpearce commented on August 19, 2024

To continue my thought, and to put more words into @mounirlamouri 's mouth, the incentive for making canAutoplay() promise-based was strictly about the return type, and not about async-vs-sync.

If that's the case, then unfortunately there's not a nice way in WebIDL to synchronously return multiple types, particularly because returning an object may be evaluated as "true-ish".

One idea (suggested by heycam) would be to invert the semantics of the API, so it returns the DOMException that a play() call would be rejected with, or null if play() would succeed. So something like:

let error = video.wouldPlayReject();
if (!error) {
  video.play();
} else {
  // handle error, fallback etc..
}

As best I can see with simple grepping through the spec and Firefox's source, play() promise is only rejected with AbortError, SrcNotSupportedError, and NotAllowedError. I am not sure of the value of exposing the other failures types, as they're avoidable; AbortError generally happens when you pause() after a play(), and NotSupportedError can be avoided by using canPlayType().

Do we really need to expose all the errors that could happen here? How much of the media load algorithm should we run here?

from autoplay.

 avatar commented on August 19, 2024

canAutoplay should not affect the state of the media element. If metadata is not loaded then we should return InvalidStateError.

One advantage is that it works well with the async play function so you can use promise chaining e.g.

video.canAutoplay()
  .then(() => video.play())
  .then(() => /* success */)
  .catch((e) => /* error */);

Here sites could catch e which would be the DOMException from either video.canAutoplay() or video.play().

from autoplay.

mounirlamouri avatar mounirlamouri commented on August 19, 2024

Having an async API is a better model. All browsers probably bend backward to make sure that they know whether something can autoplay synchronously. I would like to make sure we have the opportunity to remove this pain in the platform. By adding asynchronous APIs to detect autoplay, we may end up in a place where no one will rely on video.play(); if (video.paused).

On top of this, the promise offers a much nicer API than what is available with the sync alternative.

@cpearce what drives the request for a sync version?

from autoplay.

cpearce avatar cpearce commented on August 19, 2024

canAutoplay should not affect the state of the media element.

Agreed.

One advantage is that it works well with the async play function so you can use promise chaining e.g.

video.canAutoplay()
  .then(() => video.play())
  .then(() => /* success */)
  .catch((e) => /* error */);

I think examples are a great way to discover the consequences of design choices.

Typically I see authors fallback to muted autoplay if their audible autoplay is blocked, so I think to be more representative of uses we'll see in the wild, our examples should handle being blocked and try to fallback to muted. So the example I'd give would be something like this:

      function playWithMutedPlayFallback(video) {
        return video.canAutoplay().catch((e) => {
          console.log("canAutoPlay failed with " + e.message)
          video.muted = true;
        }).then(() => video.play());
      }

      playWithMutedPlayFallback(video)
        .then(
          () => console.log("Should be playing..."),
          (e) => console.log("cannot play: " + e.message));

In the "cannot play" catch, I'd assume authors will fallback to showing a poster frame.

As a counter example, here's how I'd do that with the synchronous variant:

      function playWithMutedPlayFallback(video) {
        if (!video.allowedToPlay) {
          console.log("Not allowed to play non-muted");
          video.muted = true;
        }
        return video.play();
      }

      playWithMutedPlayFallback(video)
        .then(
          () => console.log("Should be playing..."),
          (e) => console.log("cannot play: " + e.message));

The synchronous variant has less syntax and layers of callbacks, so I think it's easier to reason about. I certainly found it easier to write; the async variant took several refactors to slim down to something so concise, and to get the error handling aligned with the behaviour I wanted.

Here are the examples live, poly-filled:
https://cpearce.github.io/html-test-cases/proposed-canautoplay-ignore-readystate.html
https://cpearce.github.io/html-test-cases/proposed-allowedToPlay-ignore-readyState.html

If you try the live examples and look at the console logging, you'll see they always fallback to muted due to the readyState always being < HAVE_METADATA when the script polls whether it is allowed to autoplay.

So irrespective of whether we make this sync or async, I'd recommend we spec the readyState>=HAVE_METADATA check to be after the other checks that report that autoplay is allowed, i.e. the psedocode for determining whether a media element is "allowed to play" would be:

if (document/HTMLMediaElement is gesture activated) {
  // allowed to play
}
if (HTMLMediaElement has muted attribute or volume == 0) {
  // allowed to play
}
// check any other conditions that allow playback...
if (HTMLMediaElement.readyState < HAVE_METADATA) {
  // not allowed to play, "invalid state".
}
if (media resource doesn't have audio track) {
 // allowed to play
}
// otherwise, not allowed to play.

This means script can always determine whether playback will be allowed due to gesture activation even in the case where metadata isn't loaded yet. They can also determine whether muted autoplay is allowed without gesture activation, even in the case where metadata isn't loaded yet.

from autoplay.

cpearce avatar cpearce commented on August 19, 2024

Having an async API is a better model.

Promises are certainly much nicer than callbacks for operations that are asynchronous. But AFAICT, we actually don't need to do anything that's async here?

On top of this, the promise offers a much nicer API than what is available with the sync alternative.

Do you think in the examples I posted above that the async case is nicer? Can you refactor it to make it as simple as the sync example?

@cpearce what drives the request for a sync version?

Simplicity. But also as I said above, I'm wary of state changes in the media pipeline meaning the result posted to the promise callback gets out of sync with the state of the world. The canAutoplay() implementation actually being synchronous under the hood plus promise callbacks running in microtasks go a long way to helping alleviate this concern, but if canAutoplay() was actually async (that is, tasks run while decisions as to whether a media element is allowed to play or not), then readyState changes could make the results posted by this API incorrect.

from autoplay.

 avatar commented on August 19, 2024

Your synchronous variant does not take into account metadata not being loaded and handling an invalid state error. When you add that it becomes much more complicated than the async variant.

from autoplay.

cpearce avatar cpearce commented on August 19, 2024

So irrespective of whether we make this sync or async, I'd recommend we spec the readyState>=HAVE_METADATA check to be after the other checks that report that autoplay is allowed

Do you disagree with this statement?

from autoplay.

cpearce avatar cpearce commented on August 19, 2024

Your synchronous variant does not take into account metadata not being loaded and handling an invalid state error.

This was intentional.

As I understand it, the purpose of this API is to determine whether a media element can autoplay as quickly as possible. An author may wish to play a different media (or start the download of subtitles) in the case where they can't autoplay audibly. As such, waiting for the network load required for the media element to reach readyState >= HAVE_METADATA works against that goal.

When you add that it becomes much more complicated than the async variant.

I appreciate that you're trying to make your replies as concise as possible, but it would be helpful if you could backup your asserted opinions with supporting evidence.

If you want to wait for loaded metadata, so you can get a slow-but-definitive answer first time, I'd do it like this:

      function once(target, name) {
        return new Promise(function(resolve, reject) {
          target.addEventListener(name, e => resolve(e), {once: true});
        });
      }      

      let loaded_metadata = new Promise((resolve, reject)=>{
        once(video, "loadedmetadata").then(resolve);
        once(video, "error").then(e => reject(e));
      });
      loaded_metadata.then(playWithMutedPlayFallback(...))

So that would be the same in both cases. How could this be improved?

from autoplay.

mounirlamouri avatar mounirlamouri commented on August 19, 2024

As I understand it, the purpose of this API is to determine whether a media element can autoplay as quickly as possible.

This is not what we were aiming for. During our meeting at TPAC, we decided that the document-level API should be used for a quick answer and the element-level API should be used for a clear answer: "can I play now?". Because metadata are key in order to answer this question, we decided that we would require this state in order to answer the question instead of having a case where the UA answers "no, but ask me again later".

I feel that we are going in circles, trying to re-do the discussions we had for 2 hours at TPAC. @padenot and @cpearce maybe you two could talk in order to clarify the situation? It may be better than trying to rehash the same arguments over here?

from autoplay.

cpearce avatar cpearce commented on August 19, 2024

the element-level API should be used for a clear answer: "can I play now?". Because metadata are key in order to answer this question, we decided that we would require this state in order to answer the question instead of having a case where the UA answers "no, but ask me again later".

If we want to avoid having a case where the UA answers "no, but ask me again later", then we should not reject the promise with InvalidStateError if readyState has not yet reached HAVE_METADATA. That's equivalent to saying "try again later".

In order to get a definitive answer from the API as proposed in issue#1 authors would have to wait for loadedmetadata before they can safely call the API. This makes the API more likely to be incorrectly used.

So when canAutoplay() is called, and if we don't already know that playback will be allowed due to some other condition such as gesture activation, and readyState has not yet reached HAVE_METADATA, we should delay resolving the promise until metadata has been loaded.

Obviously, that would be a reason for this API to be async.

How does that sound?

from autoplay.

 avatar commented on August 19, 2024

If we can answer before we have the metadata (e.g. gesture) we should resolve immediately. However, I am concerned that not returning immediately if there is no data will result in hanging promises. Since the canAutoplay function does not actually change any state there is no guarantee that the promise will ever resolve.

What about if when we come to the readyState check we resolve the promise immediately unless the element is loading. In which case we delay resolving until either the load succeeds or fails. For example:

  1. Check gesture activation => return true if activated
  2. If muted attribute or volume == 00 => return true
  3. If we have not started loading and readyState < HAVE_METADATA => "invalid state"
  4. a. If we have started loading wait for the element to finish loading
  5. a. i. If we successfully loaded metadata then continue to (4)
  6. a. ii. If we failed to load metadata => "invalid state"
  7. If the media resource does not have an audio track => return true
  8. Otherwise, not allowed to play.

Therefore developers do not need to track the loadedmetadata and error events e.g:

video.load();
video.canAutoplay()
  .then((canAutoplay) => {
    video.muted = !canAutoplay;
    return video.play();
  })
  .then(() => /* success */)
  .catch((e) => /* error */);

from autoplay.

mounirlamouri avatar mounirlamouri commented on August 19, 2024

the element-level API should be used for a clear answer: "can I play now?". Because metadata are key in order to answer this question, we decided that we would require this state in order to answer the question instead of having a case where the UA answers "no, but ask me again later".

If we want to avoid having a case where the UA answers "no, but ask me again later", then we should not reject the promise with InvalidStateError if readyState has not yet reached HAVE_METADATA. That's equivalent to saying "try again later".

Not quite. The API would require metadata to be available. By never giving an answer unless the information is available, we are trying to make the API behaviour predictable.

So when canAutoplay() is called, and if we don't already know that playback will be allowed due to some other condition such as gesture activation, and readyState has not yet reached HAVE_METADATA, we should delay resolving the promise until metadata has been loaded.

As @rebeccahughes pointed out, this may end up with a hanging promise. The solution to wait if metadata are being loaded is somewhere in the middle but I think it would just make the API less predictable. Requiring metadata will reduce the chances of developers shooting themselves in the foot. It may end up with situations where a race condition will allow the API to work and sometimes will reject but it's not so different from sometimes saying yes and sometimes no and much less likely. Again, when use case here is that a page will try to check if they can play something without muting it and if the API says no, they will add a muted attribute to it. In this context, waiting for metadata is not a problem. The assumption is that any use case where different resources would be picked, the document-level API would be used.

from autoplay.

cpearce avatar cpearce commented on August 19, 2024

If we can answer before we have the metadata (e.g. gesture) we should resolve immediately.

Great!

However, I am concerned that not returning immediately if there is no data will result in hanging promises. Since the canAutoplay function does not actually change any state there is no guarantee that the promise will ever resolve.

What about if when we come to the readyState check we resolve the promise immediately unless the element is loading. In which case we delay resolving until either the load succeeds or fails.

Yes, this sounds like a good idea.

I agree we'd need to be careful to reject/resolve(false) in the cases where the load algorithm is running, but has stalled. For example, if the resource selection algorithm is waiting on a child to be appended to the HTMLMediaElement. I don't know how MSE affects the load algorithm as well as the src=url case, but there may be a similar case such as a MediaSource is attached but no source buffers have been added yet.

from autoplay.

cpearce avatar cpearce commented on August 19, 2024

Not quite. The API would require metadata to be available. By never giving an answer unless the information is available, we are trying to make the API behaviour predictable.

The point about metadata being loaded is that it's racy; it depends on the network. This makes it unpredictable.

As @rebeccahughes pointed out, this may end up with a hanging promise.

We should reject in the cases where there would be a hanging promise, as the resource isn't loading, there's nothing to play.

The solution to wait if metadata are being loaded is somewhere in the middle but I think it would just make the API less predictable.

It seems to me that making canAutoplay() wait until metadata had loaded would make it more predictable, not less.

Without this provision, the following code would give you different results depending on the speed of the network/cache/media pipeline:

async function () {
  let v = document.createElement("video");
  v.src = "video-without-audio-track.mp4"; // Note: starts load algorithm
  let canPlay = false;
  while (!canPlay) {
    canPlay = await v.canAutoplay().then(()=>true, ()=>false);
    console.log("canPlay=" + canPlay);
  }
}();

Whereas if we awaited loading metadata this would always give the same behaviour (unless there was a network error).

Requiring metadata will reduce the chances of developers shooting themselves in the foot.

I don't think this follows. If authors have to remember to await metadata loading before they can get an authoritative answer from canAutoplay(), then mistakes will be made and authors will forget. Doing this as part of the API itself makes it easier to use correctly, and thus more predictable.

Would you as an author ever want to intentionally call this API before the metatadata had been loaded?

from autoplay.

 avatar commented on August 19, 2024

Would you as an author ever want to intentionally call this API before the metatadata had been loaded?

I think the document level autoplayPolicy should be used if an author wants to determine before the metadata is loaded as that will return whether autoplay or muted autoplay is allowed. That should return a quick answer and then they can use canAutoplay can be used once the metadata has loaded and we have all the information to make a firm decision.

from autoplay.

alastor0325 avatar alastor0325 commented on August 19, 2024

This is an old issue, which was already solved by the TAG resolution (the conclusion is to make API sync). So close this issue.

from autoplay.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.