blob: eb35d37c830f38fef6e49bb5225be80a7cfa0495 [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 "third_party/blink/renderer/modules/media_controls/media_controls_orientation_lock_delegate.h"
#include <memory>
#include "build/build_config.h"
#include "third_party/blink/public/common/thread_safe_browser_interface_broker_proxy.h"
#include "third_party/blink/public/common/widget/screen_info.h"
#include "third_party/blink/public/platform/task_type.h"
#include "third_party/blink/renderer/core/dom/events/event.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/screen.h"
#include "third_party/blink/renderer/core/html/media/html_video_element.h"
#include "third_party/blink/renderer/core/page/chrome_client.h"
#include "third_party/blink/renderer/modules/device_orientation/device_orientation_data.h"
#include "third_party/blink/renderer/modules/device_orientation/device_orientation_event.h"
#include "third_party/blink/renderer/modules/screen_orientation/screen_orientation.h"
#include "third_party/blink/renderer/modules/screen_orientation/screen_orientation_controller.h"
#include "third_party/blink/renderer/modules/screen_orientation/screen_screen_orientation.h"
#include "third_party/blink/renderer/modules/screen_orientation/web_lock_orientation_callback.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
#include "third_party/blink/renderer/platform/wtf/functional.h"
#include "third_party/blink/renderer/platform/wtf/math_extras.h"
#if defined(OS_ANDROID)
#include "third_party/blink/public/platform/platform.h"
#include "third_party/blink/renderer/platform/mojo/mojo_helper.h"
#endif // defined(OS_ANDROID)
#undef atan2 // to use std::atan2 instead of wtf_atan2
#undef fmod // to use std::fmod instead of wtf_fmod
#include <cmath>
namespace blink {
namespace {
// WebLockOrientationCallback implementation that will not react to a success
// nor a failure.
class DummyScreenOrientationCallback : public WebLockOrientationCallback {
public:
void OnSuccess() override {}
void OnError(WebLockOrientationError) override {}
};
} // anonymous namespace
constexpr base::TimeDelta MediaControlsOrientationLockDelegate::kLockToAnyDelay;
MediaControlsOrientationLockDelegate::MediaControlsOrientationLockDelegate(
HTMLVideoElement& video)
: monitor_(video.GetExecutionContext()), video_element_(video) {
if (VideoElement().isConnected())
Attach();
}
void MediaControlsOrientationLockDelegate::Attach() {
DCHECK(VideoElement().isConnected());
GetDocument().addEventListener(event_type_names::kFullscreenchange, this,
true);
VideoElement().addEventListener(event_type_names::kWebkitfullscreenchange,
this, true);
VideoElement().addEventListener(event_type_names::kLoadedmetadata, this,
true);
}
void MediaControlsOrientationLockDelegate::Detach() {
DCHECK(!VideoElement().isConnected());
GetDocument().removeEventListener(event_type_names::kFullscreenchange, this,
true);
VideoElement().removeEventListener(event_type_names::kWebkitfullscreenchange,
this, true);
VideoElement().removeEventListener(event_type_names::kLoadedmetadata, this,
true);
}
void MediaControlsOrientationLockDelegate::MaybeLockOrientation() {
DCHECK(state_ != State::kMaybeLockedFullscreen);
if (VideoElement().getReadyState() == HTMLMediaElement::kHaveNothing) {
state_ = State::kPendingMetadata;
return;
}
state_ = State::kMaybeLockedFullscreen;
if (!GetDocument().domWindow())
return;
auto* controller =
ScreenOrientationController::From(*GetDocument().domWindow());
if (controller->MaybeHasActiveLock())
return;
locked_orientation_ = ComputeOrientationLock();
DCHECK_NE(locked_orientation_,
device::mojom::blink::ScreenOrientationLockType::DEFAULT);
controller->lock(locked_orientation_,
std::make_unique<DummyScreenOrientationCallback>());
MaybeListenToDeviceOrientation();
}
void MediaControlsOrientationLockDelegate::ChangeLockToAnyOrientation() {
// Must already be locked.
DCHECK_EQ(state_, State::kMaybeLockedFullscreen);
DCHECK_NE(locked_orientation_,
device::mojom::blink::ScreenOrientationLockType::DEFAULT);
locked_orientation_ = device::mojom::blink::ScreenOrientationLockType::ANY;
// The document could have been detached from the frame.
if (LocalDOMWindow* window = GetDocument().domWindow()) {
ScreenOrientationController::From(*window)->lock(
locked_orientation_,
std::make_unique<DummyScreenOrientationCallback>());
}
}
void MediaControlsOrientationLockDelegate::MaybeUnlockOrientation() {
DCHECK(state_ != State::kPendingFullscreen);
state_ = State::kPendingFullscreen;
if (locked_orientation_ ==
device::mojom::blink::ScreenOrientationLockType::DEFAULT /* unlocked */)
return;
monitor_.reset(); // Cancel any GotIsAutoRotateEnabledByUser Mojo callback.
LocalDOMWindow* dom_window = GetDocument().domWindow();
dom_window->removeEventListener(event_type_names::kDeviceorientation, this,
false);
ScreenOrientationController::From(*dom_window)->unlock();
locked_orientation_ =
device::mojom::blink::ScreenOrientationLockType::DEFAULT /* unlocked */;
lock_to_any_task_.Cancel();
}
void MediaControlsOrientationLockDelegate::MaybeListenToDeviceOrientation() {
DCHECK_EQ(state_, State::kMaybeLockedFullscreen);
DCHECK_NE(locked_orientation_,
device::mojom::blink::ScreenOrientationLockType::DEFAULT);
// If the rotate-to-fullscreen feature is also enabled, then start listening
// to deviceorientation events so the orientation can be unlocked once the
// user rotates the device to match the video's orientation (allowing the user
// to then exit fullscreen by rotating their device back to the opposite
// orientation). Otherwise, don't listen for deviceorientation events and just
// hold the orientation lock until the user exits fullscreen (which prevents
// the user rotating to the wrong fullscreen orientation).
if (!RuntimeEnabledFeatures::VideoRotateToFullscreenEnabled())
return;
if (is_auto_rotate_enabled_by_user_override_for_testing_ != base::nullopt) {
GotIsAutoRotateEnabledByUser(
is_auto_rotate_enabled_by_user_override_for_testing_.value());
return;
}
// Check whether the user locked screen orientation at the OS level.
#if defined(OS_ANDROID)
DCHECK(!monitor_.is_bound());
Platform::Current()->GetBrowserInterfaceBroker()->GetInterface(
monitor_.BindNewPipeAndPassReceiver(
GetDocument().GetTaskRunner(TaskType::kMediaElementEvent)));
monitor_->IsAutoRotateEnabledByUser(WTF::Bind(
&MediaControlsOrientationLockDelegate::GotIsAutoRotateEnabledByUser,
WrapPersistent(this)));
#else
GotIsAutoRotateEnabledByUser(true); // Assume always enabled on other OSes.
#endif // defined(OS_ANDROID)
}
void MediaControlsOrientationLockDelegate::GotIsAutoRotateEnabledByUser(
bool enabled) {
monitor_.reset();
if (!enabled) {
// Since the user has locked their screen orientation, prevent
// MediaControlsRotateToFullscreenDelegate from exiting fullscreen by not
// listening for deviceorientation events and instead continuing to hold the
// orientation lock until the user exits fullscreen. This enables users to
// watch videos in bed with their head facing sideways (which requires a
// landscape screen orientation when the device is portrait and vice versa).
// TODO(johnme): Ideally we would start listening for deviceorientation
// events and allow rotating to exit if a user enables screen auto rotation
// after we have locked to landscape. That would require listening for
// changes to the auto rotate setting, rather than only checking it once.
return;
}
if (LocalDOMWindow* dom_window = GetDocument().domWindow()) {
dom_window->addEventListener(event_type_names::kDeviceorientation, this,
false);
}
}
HTMLVideoElement& MediaControlsOrientationLockDelegate::VideoElement() const {
return *video_element_;
}
Document& MediaControlsOrientationLockDelegate::GetDocument() const {
return VideoElement().GetDocument();
}
void MediaControlsOrientationLockDelegate::Invoke(
ExecutionContext* execution_context,
Event* event) {
if (event->type() == event_type_names::kFullscreenchange ||
event->type() == event_type_names::kWebkitfullscreenchange) {
if (VideoElement().IsFullscreen()) {
if (state_ == State::kPendingFullscreen)
MaybeLockOrientation();
} else {
if (state_ != State::kPendingFullscreen)
MaybeUnlockOrientation();
}
return;
}
if (event->type() == event_type_names::kLoadedmetadata) {
if (state_ == State::kPendingMetadata)
MaybeLockOrientation();
return;
}
if (event->type() == event_type_names::kDeviceorientation) {
if (event->isTrusted() &&
event->InterfaceName() ==
event_interface_names::kDeviceOrientationEvent) {
MaybeLockToAnyIfDeviceOrientationMatchesVideo(
To<DeviceOrientationEvent>(event));
}
return;
}
NOTREACHED();
}
device::mojom::blink::ScreenOrientationLockType
MediaControlsOrientationLockDelegate::ComputeOrientationLock() const {
DCHECK(VideoElement().getReadyState() != HTMLMediaElement::kHaveNothing);
const unsigned width = VideoElement().videoWidth();
const unsigned height = VideoElement().videoHeight();
if (width > height)
return device::mojom::blink::ScreenOrientationLockType::LANDSCAPE;
if (height > width)
return device::mojom::blink::ScreenOrientationLockType::PORTRAIT;
// For square videos, try to lock to the current screen orientation for
// consistency. Use device::mojom::blink::ScreenOrientationLockType::LANDSCAPE
// as a fallback value.
// TODO(mlamouri): we could improve this by having direct access to
// `window.screen.orientation.type`.
LocalFrame* frame = GetDocument().GetFrame();
if (!frame)
return device::mojom::blink::ScreenOrientationLockType::LANDSCAPE;
ChromeClient& chrome_client = frame->GetChromeClient();
switch (chrome_client.GetScreenInfo(*frame).orientation_type) {
case mojom::blink::ScreenOrientation::kPortraitPrimary:
case mojom::blink::ScreenOrientation::kPortraitSecondary:
return device::mojom::blink::ScreenOrientationLockType::PORTRAIT;
case mojom::blink::ScreenOrientation::kLandscapePrimary:
case mojom::blink::ScreenOrientation::kLandscapeSecondary:
return device::mojom::blink::ScreenOrientationLockType::LANDSCAPE;
case mojom::blink::ScreenOrientation::kUndefined:
return device::mojom::blink::ScreenOrientationLockType::LANDSCAPE;
}
NOTREACHED();
return device::mojom::blink::ScreenOrientationLockType::LANDSCAPE;
}
MediaControlsOrientationLockDelegate::DeviceOrientationType
MediaControlsOrientationLockDelegate::ComputeDeviceOrientation(
DeviceOrientationData* data) const {
LocalDOMWindow* dom_window = GetDocument().domWindow();
if (!dom_window)
return DeviceOrientationType::kUnknown;
if (!data->CanProvideBeta() || !data->CanProvideGamma())
return DeviceOrientationType::kUnknown;
double beta = data->Beta();
double gamma = data->Gamma();
// Calculate the projection of the up vector (normal to the earth's surface)
// onto the device's screen in its natural orientation. (x,y) will lie within
// the unit circle centered on (0,0), e.g. if the top of the device is
// pointing upwards (x,y) will be (0,-1).
double x = -std::sin(deg2rad(gamma)) * std::cos(deg2rad(beta));
double y = -std::sin(deg2rad(beta));
// Convert (x,y) to polar coordinates: 0 <= device_orientation_angle < 360 and
// 0 <= r <= 1, such that device_orientation_angle is the clockwise angle in
// degrees between the current physical orientation of the device and the
// natural physical orientation of the device (ignoring the screen
// orientation). Thus snapping device_orientation_angle to the nearest
// multiple of 90 gives the value screen.orientation.angle would have if the
// screen orientation was allowed to rotate freely to match the device
// orientation. Note that we want device_orientation_angle==0 when the top of
// the device is pointing upwards, but atan2's zero angle points to the right,
// so we pass y=x and x=-y to atan2 to rotate by 90 degrees.
double r = std::sqrt(x * x + y * y);
double device_orientation_angle =
std::fmod(rad2deg(std::atan2(/* y= */ x, /* x= */ -y)) + 360, 360);
// If angle between device's screen and the horizontal plane is less than
// kMinElevationAngle (chosen to approximately match Android's behavior), then
// device is too flat to reliably determine orientation.
constexpr double kMinElevationAngle = 24; // degrees from horizontal plane
if (r < std::sin(deg2rad(kMinElevationAngle)))
return DeviceOrientationType::kFlat;
// device_orientation_angle snapped to nearest multiple of 90.
int device_orientation_angle90 =
static_cast<int>(std::lround(device_orientation_angle / 90) * 90);
// To be considered portrait or landscape, allow the device to be rotated 23
// degrees (chosen to approximately match Android's behavior) to either side
// of those orientations. In the remaining 90 - 2*23 = 44 degree hysteresis
// zones, consider the device to be diagonal. These hysteresis zones prevent
// the computed orientation from oscillating rapidly between portrait and
// landscape when the device is in between the two orientations.
if (std::abs(device_orientation_angle - device_orientation_angle90) > 23)
return DeviceOrientationType::kDiagonal;
// screen.orientation.angle is the standardized replacement for
// window.orientation. They are equal, except -90 was replaced by 270.
int screen_orientation_angle =
ScreenScreenOrientation::orientation(*dom_window->screen())->angle();
// This is equivalent to screen.orientation.type.startsWith('landscape').
bool screen_orientation_is_portrait =
dom_window->screen()->width() <= dom_window->screen()->height();
// The natural orientation of the device could either be portrait (almost
// all phones, and some tablets like Nexus 7) or landscape (other tablets
// like Pixel C). Detect this by comparing angle to orientation.
// TODO(johnme): This might get confused on square screens.
bool screen_orientation_is_natural_or_flipped_natural =
screen_orientation_angle % 180 == 0;
bool natural_orientation_is_portrait =
screen_orientation_is_portrait ==
screen_orientation_is_natural_or_flipped_natural;
// If natural_orientation_is_portrait_, then angles 0 and 180 are portrait,
// otherwise angles 90 and 270 are portrait.
int portrait_angle_mod_180 = natural_orientation_is_portrait ? 0 : 90;
return device_orientation_angle90 % 180 == portrait_angle_mod_180
? DeviceOrientationType::kPortrait
: DeviceOrientationType::kLandscape;
}
void MediaControlsOrientationLockDelegate::
MaybeLockToAnyIfDeviceOrientationMatchesVideo(
DeviceOrientationEvent* event) {
DCHECK_EQ(state_, State::kMaybeLockedFullscreen);
DCHECK(locked_orientation_ ==
device::mojom::blink::ScreenOrientationLockType::PORTRAIT ||
locked_orientation_ ==
device::mojom::blink::ScreenOrientationLockType::LANDSCAPE);
DeviceOrientationType device_orientation =
ComputeDeviceOrientation(event->Orientation());
DeviceOrientationType video_orientation =
locked_orientation_ ==
device::mojom::blink::ScreenOrientationLockType::PORTRAIT
? DeviceOrientationType::kPortrait
: DeviceOrientationType::kLandscape;
if (device_orientation != video_orientation)
return;
// Job done: the user rotated their device to match the orientation of the
// video that we locked to, so now we can stop listening.
if (LocalDOMWindow* dom_window = GetDocument().domWindow()) {
dom_window->removeEventListener(event_type_names::kDeviceorientation, this,
false);
}
// Delay before changing lock, as a workaround for the case where the device
// is initially portrait-primary, then fullscreen orientation lock locks it to
// landscape and the screen orientation changes to landscape-primary, but the
// user actually rotates the device to landscape-secondary. In that case, if
// this delegate unlocks the orientation before Android has detected the
// rotation to landscape-secondary (which is slow due to low-pass filtering),
// Android would change the screen orientation back to portrait-primary. This
// is avoided by delaying unlocking long enough to ensure that Android has
// detected the orientation change.
lock_to_any_task_ = PostDelayedCancellableTask(
*GetDocument().GetTaskRunner(TaskType::kMediaElementEvent), FROM_HERE,
// Conceptually, this callback will unlock the screen orientation,
// so that the user can now rotate their device to the opposite
// orientation in order to exit fullscreen. But unlocking
// corresponds to
// device::mojom::blink::ScreenOrientationLockType::DEFAULT, which is
// sometimes a specific orientation. For example in a webapp added to
// homescreen that has set its orientation to portrait using the manifest,
// unlocking actually locks to portrait, which would immediately exit
// fullscreen if we're watching a landscape video in landscape
// orientation! So instead, this locks to
// device::mojom::blink::ScreenOrientationLockType::ANY which will
// auto-rotate according to the accelerometer, and only exit
// fullscreen once the user actually rotates their device. We only
// fully unlock to
// device::mojom::blink::ScreenOrientationLockType::DEFAULT once
// fullscreen is exited.
WTF::Bind(
&MediaControlsOrientationLockDelegate::ChangeLockToAnyOrientation,
WrapPersistent(this)),
kLockToAnyDelay);
}
void MediaControlsOrientationLockDelegate::Trace(Visitor* visitor) const {
NativeEventListener::Trace(visitor);
visitor->Trace(monitor_);
visitor->Trace(video_element_);
}
} // namespace blink