blob: 42e84735a7c47cad23674c459573c88aa5e6bb5a [file] [log] [blame]
/*
* Copyright (C) 2019 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "RemotePlayback.h"
#if ENABLE(WIRELESS_PLAYBACK_TARGET)
#include "Event.h"
#include "EventNames.h"
#include "HTMLMediaElement.h"
#include "JSDOMPromiseDeferred.h"
#include "Logging.h"
#include "MediaElementSession.h"
#include "MediaPlaybackTarget.h"
#include "RemotePlaybackAvailabilityCallback.h"
#include <wtf/IsoMallocInlines.h>
namespace WebCore {
WTF_MAKE_ISO_ALLOCATED_IMPL(RemotePlayback);
Ref<RemotePlayback> RemotePlayback::create(HTMLMediaElement& element)
{
return adoptRef(*new RemotePlayback(element));
}
RemotePlayback::RemotePlayback(HTMLMediaElement& element)
: WebCore::ActiveDOMObject(element.scriptExecutionContext())
, m_mediaElement(makeWeakPtr(element))
, m_eventQueue(MainThreadGenericEventQueue::create(*this))
{
suspendIfNeeded();
}
RemotePlayback::~RemotePlayback()
{
}
void RemotePlayback::watchAvailability(Ref<RemotePlaybackAvailabilityCallback>&& callback, Ref<DeferredPromise>&& promise)
{
// 6.2.1.3 Getting the remote playback devices availability information
// https://w3c.github.io/remote-playback/#monitoring-the-list-of-available-remote-playback-devices
// W3C Editor's Draft 15 July 2016
// 1. Let promise be a new promise->
// 2. Return promise, and run the following steps below:
m_taskQueue.enqueueTask([this, callback = WTFMove(callback), promise = WTFMove(promise)] () mutable {
// 3. If the disableRemotePlayback attribute is present for the media element, reject the promise with
// InvalidStateError and abort all the remaining steps.
if (!m_mediaElement
|| m_mediaElement->hasAttributeWithoutSynchronization(HTMLNames::webkitwirelessvideoplaybackdisabledAttr)
|| m_mediaElement->hasAttributeWithoutSynchronization(HTMLNames::disableremoteplaybackAttr)) {
WTFLogAlways("RemotePlayback::watchAvailability()::task - promise rejected");
promise->reject(InvalidStateError);
return;
}
// 4. If the user agent is unable to monitor the list of available remote playback devices for the entire
// lifetime of the browsing context (for instance, because the user has disabled this feature), then run
// the following steps in parallel:
// 5. If the user agent is unable to continuously monitor the list of available remote playback devices but
// can do it for a short period of time when initiating remote playback, then:
// NOTE: Unimplemented; all current ports can support continuous device monitoring
// 6. Let callbackId be a number unique to the media element that will identify the callback.
int32_t callbackId = ++m_nextId;
// 7. Create a tuple (callbackId, callback) and add it to the set of availability callbacks for this media element.
ASSERT(!m_callbackMap.contains(callbackId));
m_callbackMap.add(callbackId, WTFMove(callback));
// 8. Fulfill promise with the callbackId and run the following steps in parallel:
promise->whenSettled([this, protectedThis = makeRefPtr(this), callbackId] {
// 8.1 Queue a task to invoke the callback with the current availability for the media element.
m_taskQueue.enqueueTask([this, callbackId] {
auto foundCallback = m_callbackMap.find(callbackId);
if (foundCallback == m_callbackMap.end())
return;
if (updateAvailability() == UpdateResults::Unchanged)
foundCallback->value->handleEvent(m_available);
});
// 8.2 Run the algorithm to monitor the list of available remote playback devices.
m_mediaElement->remoteHasAvailabilityCallbacksChanged();
});
promise->resolve<IDLLong>(callbackId);
});
}
void RemotePlayback::cancelWatchAvailability(Optional<int32_t> id, Ref<DeferredPromise>&& promise)
{
// 6.2.1.5 Stop observing remote playback devices availability
// https://w3c.github.io/remote-playback/#stop-observing-remote-playback-devices-availability
// W3C Editor's Draft 15 July 2016
// 1. Let promise be a new promise->
// 2. Return promise, and run the following steps below:
m_taskQueue.enqueueTask([this, id = WTFMove(id), promise = WTFMove(promise)] {
// 3. If the disableRemotePlayback attribute is present for the media element, reject promise with
// InvalidStateError and abort all the remaining steps.
if (!m_mediaElement
|| m_mediaElement->hasAttributeWithoutSynchronization(HTMLNames::webkitwirelessvideoplaybackdisabledAttr)
|| m_mediaElement->hasAttributeWithoutSynchronization(HTMLNames::disableremoteplaybackAttr)) {
promise->reject(InvalidStateError);
return;
}
// 4. If the parameter id is undefined, clear the set of availability callbacks.
if (!id)
m_callbackMap.clear();
else {
// 5. Otherwise, if id matches the callbackId for any entry in the set of availability callbacks,
// remove the entry from the set.
if (auto it = m_callbackMap.find(id.value()) != m_callbackMap.end())
m_callbackMap.remove(it);
// 6. Otherwise, reject promise with NotFoundError and abort all the remaining steps.
else {
promise->reject(NotFoundError);
return;
}
}
// 7. If the set of availability callbacks is now empty and there is no pending request to initiate remote
// playback, cancel any pending task to monitor the list of available remote playback devices for power
// saving purposes.
m_mediaElement->remoteHasAvailabilityCallbacksChanged();
// 8. Fulfill promise.
promise->resolve();
});
}
void RemotePlayback::prompt(Ref<DeferredPromise>&& promise)
{
// 6.2.2 Prompt user for changing remote playback statee
// https://w3c.github.io/remote-playback/#stop-observing-remote-playback-devices-availability
// W3C Editor's Draft 15 July 2016
// 1. Let promise be a new promise->
// 2. Return promise, and run the following steps below:
m_taskQueue.enqueueTask([this, promise = WTFMove(promise), processingUserGesture = UserGestureIndicator::processingUserGesture()] () mutable {
// 3. If the disableRemotePlayback attribute is present for the media element, reject the promise with
// InvalidStateError and abort all the remaining steps.
if (!m_mediaElement
|| m_mediaElement->hasAttributeWithoutSynchronization(HTMLNames::webkitwirelessvideoplaybackdisabledAttr)
|| m_mediaElement->hasAttributeWithoutSynchronization(HTMLNames::disableremoteplaybackAttr)) {
promise->reject(InvalidStateError);
return;
}
// 4. If there is already an unsettled promise from a previous call to prompt for the same media element
// or even for the same browsing context, the user agent may reject promise with an OperationError
// exception and abort all remaining steps.
// NOTE: consider implementing
// 5. OPTIONALLY, if the user agent knows a priori that showing the UI for this particular media element
// is not feasible, reject promise with a NotSupportedError and abort all remaining steps.
#if !PLATFORM(IOS)
if (m_mediaElement->readyState() < HTMLMediaElementEnums::HAVE_METADATA) {
promise->reject(NotSupportedError);
return;
}
#endif
// 6. If the algorithm isn't allowed to show a popup, reject promise with an InvalidAccessError exception
// and abort these steps.
if (!processingUserGesture) {
promise->reject(InvalidAccessError);
return;
}
// 7. If the user agent needs to show the list of available remote playback devices and is not monitoring
// the list of available remote playback devices, run the steps to monitor the list of available remote
// playback devices in parallel.
// NOTE: Monitoring enabled by adding to m_promptPromises and calling remoteHasAvailabilityCallbacksChanged().
// Meanwhile, just update availability for step 9.
updateAvailability();
// 8. If the list of available remote playback devices is empty and will remain so before the request for
// user permission is completed, reject promise with a NotFoundError exception and abort all remaining steps.
// NOTE: consider implementing (no network?)
// 9. If the state is disconnected and availability for the media element is false, reject promise with a
// NotSupportedError exception and abort all remaining steps.
if (m_state == State::Disconnected && !m_available) {
promise->reject(NotSupportedError);
return;
}
m_promptPromises.append(WTFMove(promise));
m_mediaElement->remoteHasAvailabilityCallbacksChanged();
m_mediaElement->webkitShowPlaybackTargetPicker();
// NOTE: Steps 10-12 are implemented in the following methods:
});
}
void RemotePlayback::shouldPlayToRemoteTargetChanged(bool shouldPlayToRemoteTarget)
{
// 6.2.2 Prompt user for changing remote playback state [Ctd]
// https://w3c.github.io/remote-playback/#prompt-user-for-changing-remote-playback-statee
// W3C Editor's Draft 15 July 2016
LOG(Media, "RemotePlayback::shouldPlayToRemoteTargetChanged(%p), shouldPlay(%d), promise count(%lu)", this, shouldPlayToRemoteTarget, m_promptPromises.size());
// 10. If the user picked a remote playback device device to initiate remote playback with, the user agent
// must run the following steps:
if (shouldPlayToRemoteTarget) {
// 10.1 Set the state of the remote object to connecting.
// 10.3 Queue a task to fire a simple event with the name connecting at the remote property of the media element.
// The event must not bubble, must not be cancelable, and has no default action.
setState(State::Connecting);
}
for (auto& promise : std::exchange(m_promptPromises, { })) {
// 10.2 Fulfill promise.
// 10.4 Establish a connection with the remote playback device device for the media element.
// NOTE: Implemented in establishConnection().
// 11. Otherwise, if the user chose to disconnect from the remote playback device device, the user agent
// must run the following steps:
// 11.1. Fulfill promise.
// 11.2. Run the disconnect from remote playback device algorithm for the device.
// NOTE: Implemented in disconnect().
promise->resolve();
}
if (shouldPlayToRemoteTarget)
establishConnection();
else
disconnect();
m_mediaElement->remoteHasAvailabilityCallbacksChanged();
}
void RemotePlayback::setState(State state)
{
if (m_state == state)
return;
m_state = state;
switch (m_state) {
case State::Connected:
m_eventQueue->enqueueEvent(Event::create(eventNames().connectEvent, Event::CanBubble::No, Event::IsCancelable::No));
break;
case State::Connecting:
m_eventQueue->enqueueEvent(Event::create(eventNames().connectingEvent, Event::CanBubble::No, Event::IsCancelable::No));
break;
case State::Disconnected:
m_eventQueue->enqueueEvent(Event::create(eventNames().disconnectEvent, Event::CanBubble::No, Event::IsCancelable::No));
break;
}
}
void RemotePlayback::establishConnection()
{
// 6.2.4 Establishing a connection with a remote playback device
// https://w3c.github.io/remote-playback/#establishing-a-connection-with-a-remote-playback-device
// W3C Editor's Draft 15 July 2016
// 1. If the state of remote is not equal to connecting, abort all the remaining steps.
if (m_state != State::Connecting)
return;
// 2. Request connection of remote to device. The implementation of this step is specific to the user agent.
// NOTE: Handled in MediaPlayer.
// NOTE: Continued in isPlayingToRemoteTargetChanged()
}
void RemotePlayback::disconnect()
{
// 6.2.6 Disconnecting from remote playback device
// https://w3c.github.io/remote-playback/#dfn-disconnect-from-remote-playback-device
// W3C Editor's Draft 15 July 2016
// 1. If the state of remote is disconnected, abort all remaining steps.
if (m_state == State::Disconnected)
return;
// 2. Queue a task to run the following steps:
m_taskQueue.enqueueTask([this] {
// 2.1 Request disconnection of remote from the device. Implementation is user agent specific.
// NOTE: Implemented by MediaPlayer::setWirelessPlaybackTarget()
// 2.2 Change the remote's state to disconnected.
// 2.3 Fire an event with the name disconnect at remote.
setState(State::Disconnected);
// 2.4 Synchronize the current media element state with the local playback state. Implementation is
// specific to user agent.
// NOTE: Handled by the MediaPlayer
});
}
RemotePlayback::UpdateResults RemotePlayback::updateAvailability()
{
bool available = m_mediaElement ? m_mediaElement->mediaSession().hasWirelessPlaybackTargets() : false;
if (available == m_available)
return UpdateResults::Unchanged;
availabilityChanged(available);
return UpdateResults::Changed;
}
void RemotePlayback::playbackTargetPickerWasDismissed()
{
// 6.2.2 Prompt user for changing remote playback state [Ctd]
// https://w3c.github.io/remote-playback/#stop-observing-remote-playback-devices-availability
// W3C Editor's Draft 15 July 2016
// 12. Otherwise, the user is considered to deny permission to use the device, so reject promise with NotAllowedError
// exception and hide the UI shown by the user agent
ASSERT(!m_promptPromises.isEmpty());
for (auto& promise : std::exchange(m_promptPromises, { }))
promise->reject(NotAllowedError);
m_mediaElement->remoteHasAvailabilityCallbacksChanged();
}
void RemotePlayback::isPlayingToRemoteTargetChanged(bool isPlayingToTarget)
{
// 6.2.4 Establishing a connection with a remote playback device [Ctd]
// https://w3c.github.io/remote-playback/#establishing-a-connection-with-a-remote-playback-device
// W3C Editor's Draft 15 July 2016
// 3. If connection completes successfully, queue a task to run the following steps:
if (isPlayingToTarget) {
// 3.1. Set the state of remote to connected.
// 3.2. Fire a simple event named connect at remote.
setState(State::Connected);
// 3.3 Synchronize the current media element state with the remote playback state. Implementation is
// specific to user agent.
// NOTE: Implemented by MediaPlayer.
return;
}
// 4. If connection fails, queue a task to run the following steps:
// 4.1. Set the remote playback state of remote to disconnected.
// 4.2. Fire a simple event named disconnect at remote.
setState(State::Disconnected);
}
bool RemotePlayback::hasAvailabilityCallbacks() const
{
return !m_callbackMap.isEmpty() || !m_promptPromises.isEmpty();
}
void RemotePlayback::availabilityChanged(bool available)
{
if (available == m_available)
return;
m_available = available;
m_taskQueue.enqueueTask([this] {
// Protect m_callbackMap against mutation while it's being iterated over.
Vector<Ref<RemotePlaybackAvailabilityCallback>> callbacks;
callbacks.reserveInitialCapacity(m_callbackMap.size());
// Can't use copyValuesToVector() here because Ref<> has a deleted assignment operator.
for (auto& callback : m_callbackMap.values())
callbacks.uncheckedAppend(callback.copyRef());
for (auto& callback : callbacks)
callback->handleEvent(m_available);
});
}
void RemotePlayback::invalidate()
{
m_mediaElement = nullptr;
}
const char* RemotePlayback::activeDOMObjectName() const
{
return "RemotePlayback";
}
void RemotePlayback::stop()
{
m_taskQueue.close();
m_eventQueue->close();
}
}
#endif // ENABLE(WIRELESS_PLAYBACK_TARGET)