Skip to content

Commit

Permalink
"targetavailabilitychanged" event fired even when media element has d…
Browse files Browse the repository at this point in the history
…isabledRemotePlayback = true

https://bugs.webkit.org/show_bug.cgi?id=274220
rdar://128137977

Reviewed by Eric Carlson.

The expectation is that `disableRemotePlayback = true` should cause all remote playback
events to not fire. Instead, disabling remote playback seems to have no effect on disabling
these events, and default media controls remote playback buttons still appear.

Add some utility methods to HTMLMediaElement to make it a bit cheaper to check whether
remote playback is disabled, and whether remote playback event listeners exist.

When changing the state of `disableRemotePlayback`, ensure that an event is fired saying
no remote playback targets exist, which cause media controls (including the native ones)
to hide the remote playback button.

* LayoutTests/media/airplay-target-availability-disableremoteplayback-expected.txt: Added.
* LayoutTests/media/airplay-target-availability-disableremoteplayback.html: Added.
* Source/WebCore/html/HTMLMediaElement.cpp:
(WebCore::HTMLMediaElement::~HTMLMediaElement):
(WebCore::HTMLMediaElement::attributeChanged):
(WebCore::HTMLMediaElement::setReadyState):
(WebCore::HTMLMediaElement::clearMediaPlayer):
(WebCore::HTMLMediaElement::wirelessRoutesAvailableDidChange):
(WebCore::HTMLMediaElement::enqueuePlaybackTargetAvailabilityChangedEvent):
(WebCore::HTMLMediaElement::remoteHasAvailabilityCallbacksChanged):
(WebCore::HTMLMediaElement::hasTargetAvailabilityListeners):
(WebCore::HTMLMediaElement::hasEnabledTargetAvailabilityListeners):
(WebCore::HTMLMediaElement::isWirelessPlaybackTargetDisabledChanged):
(WebCore::HTMLMediaElement::isWirelessPlaybackTargetDisabled const):
(WebCore::HTMLMediaElement::addEventListener):
(WebCore::HTMLMediaElement::removeEventListener):
* Source/WebCore/html/HTMLMediaElement.h:

Canonical link: https://commits.webkit.org/278933@main
  • Loading branch information
jernoble committed May 17, 2024
1 parent a871a8e commit 7eb36e3
Show file tree
Hide file tree
Showing 5 changed files with 235 additions and 13 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
Test that a webkitplaybacktargetavailabilitychanged is not fired when adding an event listener to a media element with disableRemotePlayback set
RUN(video = document.body.appendChild(document.createElement('video')))
RUN(video.disableRemotePlayback = true)
Promise rejected correctly OK
Correctly failed to receive targetavailabilitychanged event OK
Test completed OK

Test that a webkitplaybacktargetavailabilitychanged is fired when setting disableRemotePlayback on an element with an event listener
RUN(video = document.body.appendChild(document.createElement('video')))
EVENT(webkitplaybacktargetavailabilitychanged)
EXPECTED (event.availability == 'available') OK
RUN(video.disableRemotePlayback = true)
EVENT(webkitplaybacktargetavailabilitychanged)
EXPECTED (event.availability == 'not-available') OK
Test completed OK

Test that a webkitplaybacktargetavailabilitychanged is fired when clearing disableRemotePlayback on an element with an event listener
RUN(video = document.body.appendChild(document.createElement('video')))
RUN(video.disableRemotePlayback = true)
Promise rejected correctly OK
Correctly failed to receive targetavailabilitychanged event OK
RUN(video.disableRemotePlayback = false)
EVENT(webkitplaybacktargetavailabilitychanged)
EXPECTED (event.availability == 'available') OK
Test completed OK

Test that a webkitplaybacktargetavailabilitychanged is not received when setting disableRemotePlayback, when no targets were previously available
RUN(video = document.body.appendChild(document.createElement('video')))
EVENT(webkitplaybacktargetavailabilitychanged)
EXPECTED (event.availability == 'not-available') OK
RUN(video.disableRemotePlayback = true)
Promise rejected correctly OK
Correctly failed to receive targetavailabilitychanged event OK
Test completed OK

END OF TEST

Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
<html>
<head>
<script src='media-file.js'></script>
<script src='video-test.js'></script>
<script>
window.addEventListener('load', async event => {
await runTest().then(endTest).catch(failTest);
});

function waitForTargetAvailableToBecome(element, state, duration, message) {
return new Promise(async (resolve, reject) => {
var timeout;
var listener;

let cleanup = () => {
clearTimeout(timeout);
element.removeEventListener('webkitplaybacktargetavailabilitychanged', listener);
};

listener = event => {
if (event.availability !== state)
return;
consoleWrite(`EVENT(${event.type})`);
resolve(event);
cleanup();
};
element.addEventListener('webkitplaybacktargetavailabilitychanged', listener);

timeout = setTimeout(() => {
reject(new Error(message));
cleanup();
}, duration);
});
}

async function runTest() {
const tenSeconds = 10000;
let tests = [
async function() {
consoleWrite('Test that a webkitplaybacktargetavailabilitychanged is not fired when adding an event listener to a media element with disableRemotePlayback set');

if (window.internals)
internals.setMockMediaPlaybackTargetPickerState('Sleepy TV', 'DeviceAvailable');

run(`video = document.body.appendChild(document.createElement('video'))`);
run(`video.disableRemotePlayback = true`);

await shouldReject(waitForEventWithTimeout(video, 'webkitplaybacktargetavailabilitychanged', 100));
logResult(Success, 'Correctly failed to receive targetavailabilitychanged event')
},
async function() {
consoleWrite('Test that a webkitplaybacktargetavailabilitychanged is fired when setting disableRemotePlayback on an element with an event listener');

if (window.internals)
internals.setMockMediaPlaybackTargetPickerState('Sleepy TV', 'DeviceAvailable');

run(`video = document.body.appendChild(document.createElement('video'))`);

event = await waitForTargetAvailableToBecome(video, 'available', tenSeconds, 'Failed to receive targetavailabilitychanged event');
testExpected('event.availability', 'available');

let eventPromise = waitForTargetAvailableToBecome(video, 'not-available', tenSeconds, 'Failed to receive targetavailabilitychanged event');
run(`video.disableRemotePlayback = true`);

event = await eventPromise;
testExpected('event.availability', 'not-available');
},
async function() {
consoleWrite('Test that a webkitplaybacktargetavailabilitychanged is fired when clearing disableRemotePlayback on an element with an event listener');

if (window.internals)
internals.setMockMediaPlaybackTargetPickerState('Sleepy TV', 'DeviceAvailable');

run(`video = document.body.appendChild(document.createElement('video'))`);
run(`video.disableRemotePlayback = true`);

await shouldReject(waitForEventWithTimeout(video, 'webkitplaybacktargetavailabilitychanged', tenSeconds));
logResult(Success, 'Correctly failed to receive targetavailabilitychanged event')

let eventPromise = waitForTargetAvailableToBecome(video, 'available', tenSeconds, 'Failed to receive targetavailabilitychanged event');
run(`video.disableRemotePlayback = false`);

event = await eventPromise;
testExpected('event.availability', 'available');
},
async function() {
consoleWrite('Test that a webkitplaybacktargetavailabilitychanged is not received when setting disableRemotePlayback, when no targets were previously available');

if (window.internals)
internals.setMockMediaPlaybackTargetPickerState('Sleepy TV', 'DeviceUnavailable');

run(`video = document.body.appendChild(document.createElement('video'))`);

event = await waitForTargetAvailableToBecome(video, 'not-available', tenSeconds);
testExpected('event.availability', 'not-available');

run(`video.disableRemotePlayback = true`);

await shouldReject(waitForEventWithTimeout(video, 'webkitplaybacktargetavailabilitychanged', tenSeconds));
logResult(Success, 'Correctly failed to receive targetavailabilitychanged event')
},
];

if (window.internals)
internals.setMockMediaPlaybackTargetPickerEnabled(true);

for (var test of tests) {
try {
await test();
logResult(Success, 'Test completed');
} catch(e) {
logResult(Failed, `ERROR: ${e.message}`);
} finally {
if (window.internals)
internals.setMockMediaPlaybackTargetPickerState('Sleepy TV', 'DeviceUnavailable');
video.src = '';
video.load();
document.body.removeChild(video);
consoleWrite('');
}
}

if (window.internals) {
internals.setMockMediaPlaybackTargetPickerState('Sleepy TV', 'DeviceUnavailable');
internals.setMockMediaPlaybackTargetPickerEnabled(false);
}
}

</script>
</head>

<body>
</body>
</html>
1 change: 1 addition & 0 deletions LayoutTests/platform/ios/TestExpectations
Original file line number Diff line number Diff line change
Expand Up @@ -2585,6 +2585,7 @@ platform/ios/media/video-interruption-suspendunderlock.html [ Skip ]
media/airplay-allows-buffering.html
media/airplay-autoplay.html
media/airplay-target-availability.html
media/airplay-target-availability-disableremoteplayback.html

# needs enhanced eventSender.contextMenu() return value
webkit.org/b/116651 media/context-menu-actions.html
Expand Down
71 changes: 58 additions & 13 deletions Source/WebCore/html/HTMLMediaElement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -636,7 +636,7 @@ HTMLMediaElement::~HTMLMediaElement()
#endif

#if ENABLE(WIRELESS_PLAYBACK_TARGET)
if (hasEventListeners(eventNames().webkitplaybacktargetavailabilitychangedEvent) || m_remote->hasAvailabilityCallbacks()) {
if (hasTargetAvailabilityListeners()) {
m_hasPlaybackTargetAvailabilityListeners = false;
if (m_mediaSession)
m_mediaSession->setHasPlaybackTargetAvailabilityListeners(false);
Expand Down Expand Up @@ -905,12 +905,13 @@ void HTMLMediaElement::attributeChanged(const QualifiedName& name, const AtomStr
mediaSession().setWirelessVideoPlaybackDisabled(newValue != nullAtom());
FALLTHROUGH;
case AttributeNames::disableremoteplaybackAttr:
#if ENABLE(MEDIA_SOURCE)
case AttributeNames::webkitairplayAttr:
isWirelessPlaybackTargetDisabledChanged();
#if ENABLE(MEDIA_SOURCE)
if (RefPtr mediaSource = m_mediaSource; mediaSource && isWirelessPlaybackTargetDisabled())
mediaSource->openIfDeferredOpen();
break;
#endif
break;
#endif
default:
break;
Expand Down Expand Up @@ -3041,7 +3042,7 @@ void HTMLMediaElement::setReadyState(MediaPlayer::ReadyState state)
}

#if ENABLE(WIRELESS_PLAYBACK_TARGET)
if (hasEventListeners(eventNames().webkitplaybacktargetavailabilitychangedEvent))
if (hasEnabledTargetAvailabilityListeners())
enqueuePlaybackTargetAvailabilityChangedEvent(EnqueueBehavior::OnlyWhenChanged);
#endif
m_initiallyMuted = m_volume < 0.05 || muted();
Expand Down Expand Up @@ -6478,14 +6479,15 @@ void HTMLMediaElement::clearMediaPlayer()
forgetResourceSpecificTracks();

#if ENABLE(WIRELESS_PLAYBACK_TARGET)
if (hasEventListeners(eventNames().webkitplaybacktargetavailabilitychangedEvent) || m_remote->hasAvailabilityCallbacks()) {
if (hasTargetAvailabilityListeners()) {
m_hasPlaybackTargetAvailabilityListeners = false;
if (m_mediaSession)
m_mediaSession->setHasPlaybackTargetAvailabilityListeners(false);

// Send an availability event in case scripts want to hide the picker when the element
// doesn't support playback to a target.
enqueuePlaybackTargetAvailabilityChangedEvent(EnqueueBehavior::Always);
if (!isWirelessPlaybackTargetDisabled())
enqueuePlaybackTargetAvailabilityChangedEvent(EnqueueBehavior::Always);
}

if (m_isPlayingToWirelessTarget)
Expand Down Expand Up @@ -6780,6 +6782,9 @@ void HTMLMediaElement::webkitShowPlaybackTargetPicker()

void HTMLMediaElement::wirelessRoutesAvailableDidChange()
{
if (isWirelessPlaybackTargetDisabled())
return;

bool hasTargets = mediaSession().hasWirelessPlaybackTargets();
Ref { m_remote }->availabilityChanged(hasTargets);

Expand Down Expand Up @@ -6823,7 +6828,7 @@ void HTMLMediaElement::setIsPlayingToWirelessTarget(bool isPlayingToWirelessTarg

void HTMLMediaElement::enqueuePlaybackTargetAvailabilityChangedEvent(EnqueueBehavior behavior)
{
bool hasTargets = m_mediaSession && mediaSession().hasWirelessPlaybackTargets();
bool hasTargets = !isWirelessPlaybackTargetDisabled() && m_mediaSession && mediaSession().hasWirelessPlaybackTargets();
if (behavior == EnqueueBehavior::OnlyWhenChanged && hasTargets == m_lastTargetAvailabilityEventState)
return;

Expand Down Expand Up @@ -6861,7 +6866,7 @@ void HTMLMediaElement::playbackTargetPickerWasDismissed()

void HTMLMediaElement::remoteHasAvailabilityCallbacksChanged()
{
bool hasListeners = hasEventListeners(eventNames().webkitplaybacktargetavailabilitychangedEvent) || m_remote->hasAvailabilityCallbacks();
bool hasListeners = hasEnabledTargetAvailabilityListeners();
if (m_hasPlaybackTargetAvailabilityListeners == hasListeners)
return;

Expand Down Expand Up @@ -6889,11 +6894,48 @@ bool HTMLMediaElement::hasWirelessPlaybackTargetAlternative() const
return false;
}

bool HTMLMediaElement::isWirelessPlaybackTargetDisabled() const
bool HTMLMediaElement::hasTargetAvailabilityListeners()
{
return equalLettersIgnoringASCIICase(attributeWithoutSynchronization(HTMLNames::webkitairplayAttr), "deny"_s)
return hasEventListeners(eventNames().webkitplaybacktargetavailabilitychangedEvent) || m_remote->hasAvailabilityCallbacks();
}

bool HTMLMediaElement::hasEnabledTargetAvailabilityListeners()
{
return !m_wirelessPlaybackTargetDisabled && hasTargetAvailabilityListeners();
}

void HTMLMediaElement::isWirelessPlaybackTargetDisabledChanged()
{
bool disabled = equalLettersIgnoringASCIICase(attributeWithoutSynchronization(HTMLNames::webkitairplayAttr), "deny"_s)
|| hasAttributeWithoutSynchronization(HTMLNames::webkitwirelessvideoplaybackdisabledAttr)
|| hasAttributeWithoutSynchronization(HTMLNames::disableremoteplaybackAttr);
if (m_wirelessPlaybackTargetDisabled == disabled)
return;

m_wirelessPlaybackTargetDisabled = disabled;

if (!m_wirelessPlaybackTargetDisabled && hasTargetAvailabilityListeners()) {
m_hasPlaybackTargetAvailabilityListeners = true;
mediaSession().setActive(true);
mediaSession().setHasPlaybackTargetAvailabilityListeners(true);
enqueuePlaybackTargetAvailabilityChangedEvent(EnqueueBehavior::Always);
} else if (m_wirelessPlaybackTargetDisabled && hasTargetAvailabilityListeners()) {
m_hasPlaybackTargetAvailabilityListeners = false;
mediaSession().setHasPlaybackTargetAvailabilityListeners(false);

// If the client has disabled remote playback, also has availability listeners,
// and the last state sent to the client was that targets were available,
// fire one last event indicating no pickable targets exist. This has the effect
// of having players disable their remote playback picker buttons.
if (m_lastTargetAvailabilityEventState)
enqueuePlaybackTargetAvailabilityChangedEvent(EnqueueBehavior::Always);
}
scheduleUpdateMediaState();
}

bool HTMLMediaElement::isWirelessPlaybackTargetDisabled() const
{
return m_wirelessPlaybackTargetDisabled;
}

#endif // ENABLE(WIRELESS_PLAYBACK_TARGET)
Expand Down Expand Up @@ -6931,11 +6973,14 @@ bool HTMLMediaElement::addEventListener(const AtomString& eventType, Ref<EventLi
if (eventType != eventNames().webkitplaybacktargetavailabilitychangedEvent)
return Node::addEventListener(eventType, WTFMove(listener), options);

bool isFirstAvailabilityChangedListener = !hasEventListeners(eventNames().webkitplaybacktargetavailabilitychangedEvent) && !m_remote->hasAvailabilityCallbacks();
bool isFirstAvailabilityChangedListener = !hasTargetAvailabilityListeners();

if (!Node::addEventListener(eventType, WTFMove(listener), options))
return false;

if (isWirelessPlaybackTargetDisabled())
return true;

if (isFirstAvailabilityChangedListener) {
m_hasPlaybackTargetAvailabilityListeners = true;
mediaSession().setActive(true);
Expand Down Expand Up @@ -6966,7 +7011,7 @@ bool HTMLMediaElement::removeEventListener(const AtomString& eventType, EventLis
if (!listenerWasRemoved)
return false;

bool didRemoveLastAvailabilityChangedListener = !hasEventListeners(eventNames().webkitplaybacktargetavailabilitychangedEvent) && !m_remote->hasAvailabilityCallbacks();
bool didRemoveLastAvailabilityChangedListener = !hasTargetAvailabilityListeners();
ALWAYS_LOG(LOGIDENTIFIER, "removed last listener = ", didRemoveLastAvailabilityChangedListener);
if (didRemoveLastAvailabilityChangedListener) {
m_hasPlaybackTargetAvailabilityListeners = false;
Expand Down Expand Up @@ -7710,7 +7755,7 @@ void HTMLMediaElement::createMediaPlayer() WTF_IGNORES_THREAD_SAFETY_ANALYSIS
#endif

#if ENABLE(WIRELESS_PLAYBACK_TARGET)
if (hasEventListeners(eventNames().webkitplaybacktargetavailabilitychangedEvent) || m_remote->hasAvailabilityCallbacks()) {
if (hasEnabledTargetAvailabilityListeners()) {
m_hasPlaybackTargetAvailabilityListeners = true;
mediaSession().setHasPlaybackTargetAvailabilityListeners(true);
enqueuePlaybackTargetAvailabilityChangedEvent(EnqueueBehavior::Always); // Ensure the event listener gets at least one event.
Expand Down
5 changes: 5 additions & 0 deletions Source/WebCore/html/HTMLMediaElement.h
Original file line number Diff line number Diff line change
Expand Up @@ -461,10 +461,14 @@ class HTMLMediaElement
void playbackTargetPickerWasDismissed() override;
bool hasWirelessPlaybackTargetAlternative() const;
bool isWirelessPlaybackTargetDisabled() const;
void isWirelessPlaybackTargetDisabledChanged();
bool hasTargetAvailabilityListeners();
bool hasEnabledTargetAvailabilityListeners();
#endif

bool isPlayingToWirelessPlaybackTarget() const override { return m_isPlayingToWirelessTarget; };
void setIsPlayingToWirelessTarget(bool);

bool webkitCurrentPlaybackTargetIsWireless() const;

void setPlayingOnSecondScreen(bool value);
Expand Down Expand Up @@ -1342,6 +1346,7 @@ class HTMLMediaElement

std::optional<RemotePlaybackConfiguration> m_remotePlaybackConfiguration;

bool m_wirelessPlaybackTargetDisabled { false };
bool m_isPlayingToWirelessTarget { false };
bool m_playingOnSecondScreen { false };
bool m_removedBehaviorRestrictionsAfterFirstUserGesture { false };
Expand Down

0 comments on commit 7eb36e3

Please sign in to comment.