blob: 064031f0bd16ee9b61ac4b0454d3ad14f621648a [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "modules/remoteplayback/RemotePlayback.h"
#include "bindings/core/v8/ScriptPromiseResolver.h"
#include "bindings/modules/v8/RemotePlaybackAvailabilityCallback.h"
#include "core/HTMLNames.h"
#include "core/dom/DOMException.h"
#include "core/dom/Document.h"
#include "core/dom/ExecutionContextTask.h"
#include "core/events/Event.h"
#include "core/html/HTMLMediaElement.h"
#include "modules/EventTargetModules.h"
#include "platform/MemoryCoordinator.h"
#include "platform/UserGestureIndicator.h"
namespace blink {
namespace {
const AtomicString& remotePlaybackStateToString(WebRemotePlaybackState state) {
DEFINE_STATIC_LOCAL(const AtomicString, connectingValue, ("connecting"));
DEFINE_STATIC_LOCAL(const AtomicString, connectedValue, ("connected"));
DEFINE_STATIC_LOCAL(const AtomicString, disconnectedValue, ("disconnected"));
switch (state) {
case WebRemotePlaybackState::Connecting:
return connectingValue;
case WebRemotePlaybackState::Connected:
return connectedValue;
case WebRemotePlaybackState::Disconnected:
return disconnectedValue;
}
ASSERT_NOT_REACHED();
return disconnectedValue;
}
} // anonymous namespace
// static
RemotePlayback* RemotePlayback::create(HTMLMediaElement& element) {
return new RemotePlayback(element);
}
RemotePlayback::RemotePlayback(HTMLMediaElement& element)
: ActiveScriptWrappable(this),
m_state(element.isPlayingRemotely()
? WebRemotePlaybackState::Connected
: WebRemotePlaybackState::Disconnected),
m_availability(WebRemotePlaybackAvailability::Unknown),
m_mediaElement(&element) {}
const AtomicString& RemotePlayback::interfaceName() const {
return EventTargetNames::RemotePlayback;
}
ExecutionContext* RemotePlayback::getExecutionContext() const {
return &m_mediaElement->document();
}
ScriptPromise RemotePlayback::watchAvailability(
ScriptState* scriptState,
RemotePlaybackAvailabilityCallback* callback) {
ScriptPromiseResolver* resolver = ScriptPromiseResolver::create(scriptState);
ScriptPromise promise = resolver->promise();
if (m_mediaElement->fastHasAttribute(HTMLNames::disableremoteplaybackAttr)) {
resolver->reject(DOMException::create(
InvalidStateError, "disableRemotePlayback attribute is present."));
return promise;
}
if (MemoryCoordinator::isLowEndDevice()) {
resolver->reject(DOMException::create(
NotSupportedError,
"Availability monitoring is not supported on this device."));
return promise;
}
int id;
do {
id = getExecutionContext()->circularSequentialID();
} while (!m_availabilityCallbacks
.add(id, TraceWrapperMember<RemotePlaybackAvailabilityCallback>(
this, callback))
.isNewEntry);
// Report the current availability via the callback.
getExecutionContext()->postTask(
BLINK_FROM_HERE,
createSameThreadTask(&RemotePlayback::notifyInitialAvailability,
wrapPersistent(this), id),
"watchAvailabilityCallback");
// TODO(avayvod): Currently the availability is tracked for each media element
// as soon as it's created, we probably want to limit that to when the
// page/element is visible (see https://crbug.com/597281) and has default
// controls. If there are no default controls, we should also start tracking
// availability on demand meaning the Promise returned by watchAvailability()
// will be resolved asynchronously.
resolver->resolve(id);
return promise;
}
ScriptPromise RemotePlayback::cancelWatchAvailability(ScriptState* scriptState,
int id) {
ScriptPromiseResolver* resolver = ScriptPromiseResolver::create(scriptState);
ScriptPromise promise = resolver->promise();
if (m_mediaElement->fastHasAttribute(HTMLNames::disableremoteplaybackAttr)) {
resolver->reject(DOMException::create(
InvalidStateError, "disableRemotePlayback attribute is present."));
return promise;
}
auto iter = m_availabilityCallbacks.find(id);
if (iter == m_availabilityCallbacks.end()) {
resolver->reject(DOMException::create(
NotFoundError, "A callback with the given id is not found."));
return promise;
}
m_availabilityCallbacks.remove(iter);
resolver->resolve();
return promise;
}
ScriptPromise RemotePlayback::cancelWatchAvailability(
ScriptState* scriptState) {
ScriptPromiseResolver* resolver = ScriptPromiseResolver::create(scriptState);
ScriptPromise promise = resolver->promise();
if (m_mediaElement->fastHasAttribute(HTMLNames::disableremoteplaybackAttr)) {
resolver->reject(DOMException::create(
InvalidStateError, "disableRemotePlayback attribute is present."));
return promise;
}
m_availabilityCallbacks.clear();
resolver->resolve();
return promise;
}
ScriptPromise RemotePlayback::prompt(ScriptState* scriptState) {
ScriptPromiseResolver* resolver = ScriptPromiseResolver::create(scriptState);
ScriptPromise promise = resolver->promise();
if (m_mediaElement->fastHasAttribute(HTMLNames::disableremoteplaybackAttr)) {
resolver->reject(DOMException::create(
InvalidStateError, "disableRemotePlayback attribute is present."));
return promise;
}
if (m_promptPromiseResolver) {
resolver->reject(DOMException::create(
OperationError,
"A prompt is already being shown for this media element."));
return promise;
}
if (!UserGestureIndicator::utilizeUserGesture()) {
resolver->reject(DOMException::create(
InvalidAccessError, "RemotePlayback::prompt() requires user gesture."));
return promise;
}
// TODO(avayvod): don't do this check on low-end devices - merge with
// https://codereview.chromium.org/2475293003
if (m_availability == WebRemotePlaybackAvailability::DeviceNotAvailable) {
resolver->reject(DOMException::create(NotFoundError,
"No remote playback devices found."));
return promise;
}
if (m_availability == WebRemotePlaybackAvailability::SourceNotSupported ||
m_availability == WebRemotePlaybackAvailability::SourceNotCompatible) {
resolver->reject(DOMException::create(
NotSupportedError,
"The currentSrc is not compatible with remote playback"));
return promise;
}
if (m_state == WebRemotePlaybackState::Disconnected) {
m_promptPromiseResolver = resolver;
m_mediaElement->requestRemotePlayback();
} else {
m_promptPromiseResolver = resolver;
m_mediaElement->requestRemotePlaybackControl();
}
return promise;
}
String RemotePlayback::state() const {
return remotePlaybackStateToString(m_state);
}
bool RemotePlayback::hasPendingActivity() const {
// TODO(haraken): This check should be moved to ActiveScriptWrappable.
if (getExecutionContext()->isContextDestroyed())
return false;
return hasEventListeners() || !m_availabilityCallbacks.isEmpty() ||
m_promptPromiseResolver;
}
void RemotePlayback::notifyInitialAvailability(int callbackId) {
// May not find the callback if the website cancels it fast enough.
auto iter = m_availabilityCallbacks.find(callbackId);
if (iter == m_availabilityCallbacks.end())
return;
iter->value->call(this, remotePlaybackAvailable());
}
void RemotePlayback::stateChanged(WebRemotePlaybackState state) {
if (m_state == state)
return;
if (m_promptPromiseResolver) {
// Changing state to Disconnected from "disconnected" or "connecting" means
// that establishing connection with remote playback device failed.
// Changing state to anything else means the state change intended by
// prompt() succeeded.
if (m_state != WebRemotePlaybackState::Connected &&
state == WebRemotePlaybackState::Disconnected) {
m_promptPromiseResolver->reject(DOMException::create(
AbortError, "Failed to connect to the remote device."));
} else {
DCHECK((m_state == WebRemotePlaybackState::Disconnected &&
state == WebRemotePlaybackState::Connecting) ||
(m_state == WebRemotePlaybackState::Connected &&
state == WebRemotePlaybackState::Disconnected));
m_promptPromiseResolver->resolve();
}
m_promptPromiseResolver = nullptr;
}
m_state = state;
switch (m_state) {
case WebRemotePlaybackState::Connecting:
dispatchEvent(Event::create(EventTypeNames::connecting));
break;
case WebRemotePlaybackState::Connected:
dispatchEvent(Event::create(EventTypeNames::connect));
break;
case WebRemotePlaybackState::Disconnected:
dispatchEvent(Event::create(EventTypeNames::disconnect));
break;
}
}
void RemotePlayback::availabilityChanged(
WebRemotePlaybackAvailability availability) {
if (m_availability == availability)
return;
bool oldAvailability = remotePlaybackAvailable();
m_availability = availability;
bool newAvailability = remotePlaybackAvailable();
if (newAvailability == oldAvailability)
return;
for (auto& callback : m_availabilityCallbacks.values())
callback->call(this, newAvailability);
}
void RemotePlayback::promptCancelled() {
if (!m_promptPromiseResolver)
return;
m_promptPromiseResolver->reject(
DOMException::create(NotAllowedError, "The prompt was dismissed."));
m_promptPromiseResolver = nullptr;
}
bool RemotePlayback::remotePlaybackAvailable() const {
return m_availability == WebRemotePlaybackAvailability::DeviceAvailable;
}
void RemotePlayback::remotePlaybackDisabled() {
if (m_promptPromiseResolver) {
m_promptPromiseResolver->reject(DOMException::create(
InvalidStateError, "disableRemotePlayback attribute is present."));
m_promptPromiseResolver = nullptr;
}
m_availabilityCallbacks.clear();
if (m_state != WebRemotePlaybackState::Disconnected)
m_mediaElement->requestRemotePlaybackStop();
}
void RemotePlayback::setV8ReferencesForCallbacks(
v8::Isolate* isolate,
const v8::Persistent<v8::Object>& wrapper) {
for (auto callback : m_availabilityCallbacks.values())
callback->setWrapperReference(isolate, wrapper);
}
DEFINE_TRACE(RemotePlayback) {
visitor->trace(m_availabilityCallbacks);
visitor->trace(m_promptPromiseResolver);
visitor->trace(m_mediaElement);
EventTargetWithInlineData::trace(visitor);
}
DEFINE_TRACE_WRAPPERS(RemotePlayback) {
for (auto callback : m_availabilityCallbacks.values()) {
visitor->traceWrappers(callback);
}
EventTargetWithInlineData::traceWrappers(visitor);
}
} // namespace blink