hls.js icon indicating copy to clipboard operation
hls.js copied to clipboard

ManagedMediaSource + disableRemotePlayback in Safari

Open k-g-a opened this issue 1 year ago • 7 comments

What do you want to do with Hls.js?

Hello!

TL;DR version:

There is a comment in buffer-controller around lines 206-209, whichs states: ManagedMediaSource will not open without disableRemotePlayback set to false or source alternatives

And right below there is a code: media.disableRemotePlayback = media.disableRemotePlayback || (MMS && ms instanceof MMS); Which effectively sets disableRemotePlayback to true if ManagedMediaSource is supported. This seems to be the contrary to whats said in the comment.

Even setting disableRemotePlayback to false explicitly in the user-land code won't help (because its ||, not ??).

The question is: was it intentional or is it a bug?

The full story

We are using hls.js within our own React component, initializing it right after the <video> element appears in DOM (by initializing we may assume simply calling hls.attachMedia(videoElement)).

While using ManagedMediaSource (by not disabling it explicilty via config) we experience the following errors in Safari (17.2.1 on macOS 14.2.1): Unhandled Promise Rejection: InvaldStateError. This error is in promise and comes from the native code - so there is no way we can normally catch it.

It worths mentioning, that the error does not break anything - the player (and underlying hls.js) are actually working without an issue. And it's purely a Safari issue.

We've managed to pinpoint the problem to exactly one line (the one, mentioned above), which sets disableRemotePlayback to true (as ms instanceof MMS returns true). It seems, for some reason, to be illegal in Safari right after the DOM insertion.

Another possible solution is to delay the hls.attachMedia(videoElement) call. Moving it to the next event loop seems enough. There was an idea, that media element's readyState is the issue (i.e. setting disableRemotePlayback to true is not allowed while it euqals 0 or something like that), but it actually does not change after the delay. So at the moment we have no valid guess about "why should we wait for the next event loop after the video element is in DOM before setting disableRemotePlayback to true?".

Could not reproduce it on demosite. I did not peek into the demo code, but may assume that the <video> element is present on the page for some time before the hls.js is attached.

k-g-a avatar Feb 07 '24 14:02 k-g-a

The comment should read that "disableRemotePlayback must be added to the media element when an alternate AirPlay source is not provided for ManagedMediaSource to open."

You could file an issue with WebKit about the DOM exception. Are you sure it's not tied to a call to play()? Is your app doing something on attaching/attached events?

Have you tried adding an AirPlay source element to the media element (pointing to the m3u8)?

Can you provide an example on codepen.io that reproduces the issue?

robwalch avatar Feb 07 '24 16:02 robwalch

Hi, @robwalch!

Are you sure it's not tied to a call to play()?

Yes, there is no immediate play() (and no autoplay).

Is your app doing something on attaching/attached events?

No, we only add play/playing/waiting/pause handlers to the media element.

Have you tried adding an AirPlay source element to the media element (pointing to the m3u8)?

Nope, we're not dealing with any AirPlay atm.

While composing the codepen i've found that everything works as expected. So I'm trying to figure out the true reason now.

Thanks for your time and efforts!

k-g-a avatar Feb 08 '24 08:02 k-g-a

I've managed to pinpoint the issue down to the media-chrome.

As soon as we set disableRemotePlayback via attribute (and not as a property) - the same exception arises in chrome and it has much more details (including stacktrace).

The real exception was

Uncaught (in promise) DOMException: Failed to execute 'watchAvailability' on 'RemotePlayback': disableRemotePlayback attribute is present.

And is originated from media-chrome source code.

I'm sorry for filing it up here too early. Thanks for your time!

k-g-a avatar Feb 08 '24 09:02 k-g-a

A short report on investigation for the future readers.

RemotePlayback API is looking for a disableRemotePlayback attribute (not property) to raise errors.

In Safari, for a custom element (i.e. ones created via customElements.define('some-video', Class)), if you set a property it's also being set as an attribute - thats the reason why we saw errors in Safari only, but not in Chrome.

As for the delay that helps to suppress the error: media-chrome calls for media.remote.watchAvailability API right after the custom element's connected Promise resolution (i.e. connected.then(doMoreStuff).then(call watchAvailability API)). Thus if we call hls.attachMedia(...) in the same loop, the code within that call sets disableRemotePlayback=true right before the promise chain continues to resolve. So by the moment media-chrome tries to interact with RemotePlayback API, there is already a property (in any browser) and the respective attribute (in Safari) which causes exception.

k-g-a avatar Feb 08 '24 10:02 k-g-a

Thanks for the info @k-g-a. I've asked the folks working on media-chrome to suggest a workaround (maybe you've found one?). Hopefully we'll get a fix and maybe even discover the best way to setup everything with or without remote playback (adding the AirPlay or disableRemotePlayback) from the start or following attachMedia.

HLS.js should only set disableRemotePlayback and append a source element with src=blob{MSource} when ManagedMediaSource is used (as apposed to MediaSource). Unless Chrome has added ManagedMediaSource, I would not expect HLS.js to set disableRemotePlayback. With MediaSource, src=blob should be set directly on the media element rather than appending a source child (as it always has).

robwalch avatar Feb 08 '24 17:02 robwalch

@k-g-a thanks for reporting!

could you open an issue in the https://github.com/muxinc/media-chrome repo with a reproduction online?

that will help speed up fixing this issue, we might have to check that attribute or property before calling media.remote.watchAvailability or add a try / catch in our code.

luwes avatar Feb 08 '24 17:02 luwes

Unless Chrome has added ManagedMediaSource, I would not expect HLS.js to set disableRemotePlayback

Good catch! Thats the real reason for this attribute not being present in chrome unless explicitly specified.

I'll report the issue at media-chrome within a day. Seems like the attribute check is the right way to go.

k-g-a avatar Feb 09 '24 04:02 k-g-a