hls.js
hls.js copied to clipboard
ManagedMediaSource + disableRemotePlayback in Safari
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.
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?
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!
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!
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.
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
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).
@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.
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.