blob: 16ed0d8324ff17cbd2ea7bc380f4523409821800 [file] [log] [blame]
// Copyright 2017 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/core/html/media/media_custom_controls_fullscreen_detector.h"
#include "third_party/blink/public/platform/task_type.h"
#include "third_party/blink/public/platform/web_fullscreen_video_status.h"
#include "third_party/blink/renderer/core/dom/events/event.h"
#include "third_party/blink/renderer/core/fullscreen/fullscreen.h"
#include "third_party/blink/renderer/core/html/media/html_video_element.h"
#include "third_party/blink/renderer/core/intersection_observer/intersection_observer_entry.h"
#include "third_party/blink/renderer/core/layout/layout_object.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
#include "third_party/blink/renderer/platform/wtf/functional.h"
namespace blink {
using blink::WebFullscreenVideoStatus;
namespace {
// If a video takes more that this much of the viewport, it's counted as
// fullscreen without applying the fullscreen heuristics.
// (Assuming we're in the fullscreen mode.)
constexpr float kMostlyFillViewportIntersectionThreshold = 0.85f;
// If a video takes less that this much of the viewport, we don't
// apply the fullscreen heuristics and just declare it not fullscreen.
// A portrait ultrawide video (21:9) playing on a landscape ultrawide screen
// takes about 18% of the screen, that's why 15% looks like a reasonable
// lowerbound of a real-world fullscreen video.
constexpr float kMinPossibleFullscreenIntersectionThreshold = 0.15f;
// This is how much of the viewport around the video can be taken by
// margins and framing for it to still be counted as fullscreen.
// It is measured only in the dominant direction, because of potential ratio
// mismatch that would cause big margins in the other direction.
// For example: portrain video on a landscape screen.
constexpr float kMaxAllowedVideoMarginRatio = 0.15;
// This is how much of the video can be hidden by something
// before it is nor longer counted as fullscreen.
// This helps to disregard custom controls, ads, accidental markup mistakes.
constexpr float kMaxAllowedPortionOfVideoOffScreen = 0.25;
// This heuristic handles a case of videos with an aspect ratio
// different from the screen's aspect ratio.
// Examples: A 4:3 video playing on a 16:9 screen.
// A portrait video playing on a landscape screen.
// In a nutshell:
// 1. The video should occupy most of the viewport in at least one dimension.
// 2. The video should be almost fully visible on the screen.
bool IsFullscreenVideoOfDifferentRatio(const IntSize& video_size,
const IntSize& viewport_size,
const IntSize& intersection_size) {
if (video_size.IsEmpty() || viewport_size.IsEmpty())
return false;
const float x_occupation_proportion =
1.0f * intersection_size.Width() / viewport_size.Width();
const float y_occupation_proportion =
1.0f * intersection_size.Height() / viewport_size.Height();
// The video should occupy most of the viewport in at least one dimension.
if (std::max(x_occupation_proportion, y_occupation_proportion) <
(1.0 - kMaxAllowedVideoMarginRatio)) {
return false;
}
// The video should be almost fully visible on the screen.
return video_size.Area() * (1.0 - kMaxAllowedPortionOfVideoOffScreen) <=
intersection_size.Area();
}
} // anonymous namespace
MediaCustomControlsFullscreenDetector::MediaCustomControlsFullscreenDetector(
HTMLVideoElement& video)
: video_element_(video), viewport_intersection_observer_(nullptr) {
if (VideoElement().isConnected())
Attach();
}
void MediaCustomControlsFullscreenDetector::Attach() {
VideoElement().addEventListener(event_type_names::kLoadedmetadata, this,
true);
VideoElement().GetDocument().addEventListener(
event_type_names::kWebkitfullscreenchange, this, true);
VideoElement().GetDocument().addEventListener(
event_type_names::kFullscreenchange, this, true);
// Ideally we'd like to monitor all minute intersection changes here,
// because any change can potentially affect the fullscreen heuristics,
// but it's not practical from perf point of view. Given that the heuristics
// are more of a guess that exact science, it wouldn't be well spent CPU
// cycles anyway. That's why the observer only triggers on 10% steps in
// viewport area occupation.
const WTF::Vector<float> thresholds{
kMinPossibleFullscreenIntersectionThreshold,
0.2,
0.3,
0.4,
0.5,
0.6,
0.7,
0.8,
kMostlyFillViewportIntersectionThreshold};
viewport_intersection_observer_ = IntersectionObserver::Create(
{}, thresholds, &(video_element_->GetDocument()),
WTF::BindRepeating(
&MediaCustomControlsFullscreenDetector::OnIntersectionChanged,
WrapWeakPersistent(this)),
LocalFrameUkmAggregator::kMediaIntersectionObserver,
IntersectionObserver::kDeliverDuringPostLifecycleSteps,
IntersectionObserver::kFractionOfRoot, 0, false, true);
viewport_intersection_observer_->observe(&VideoElement());
}
void MediaCustomControlsFullscreenDetector::Detach() {
if (viewport_intersection_observer_) {
viewport_intersection_observer_->disconnect();
viewport_intersection_observer_ = nullptr;
}
VideoElement().removeEventListener(event_type_names::kLoadedmetadata, this,
true);
VideoElement().GetDocument().removeEventListener(
event_type_names::kWebkitfullscreenchange, this, true);
VideoElement().GetDocument().removeEventListener(
event_type_names::kFullscreenchange, this, true);
VideoElement().SetIsEffectivelyFullscreen(
WebFullscreenVideoStatus::kNotEffectivelyFullscreen);
}
void MediaCustomControlsFullscreenDetector::Invoke(ExecutionContext* context,
Event* event) {
DCHECK(event->type() == event_type_names::kLoadedmetadata ||
event->type() == event_type_names::kWebkitfullscreenchange ||
event->type() == event_type_names::kFullscreenchange);
// Video is not loaded yet.
if (VideoElement().getReadyState() < HTMLMediaElement::kHaveMetadata)
return;
TriggerObservation();
}
void MediaCustomControlsFullscreenDetector::ContextDestroyed() {
Detach();
}
void MediaCustomControlsFullscreenDetector::ReportEffectivelyFullscreen(
bool effectively_fullscreen) {
if (!effectively_fullscreen) {
VideoElement().SetIsEffectivelyFullscreen(
WebFullscreenVideoStatus::kNotEffectivelyFullscreen);
return;
}
// Picture-in-Picture can be disabled by the website when the API is enabled.
bool picture_in_picture_allowed =
!RuntimeEnabledFeatures::PictureInPictureEnabled() &&
!VideoElement().FastHasAttribute(
html_names::kDisablepictureinpictureAttr);
if (picture_in_picture_allowed) {
VideoElement().SetIsEffectivelyFullscreen(
WebFullscreenVideoStatus::kFullscreenAndPictureInPictureEnabled);
} else {
VideoElement().SetIsEffectivelyFullscreen(
WebFullscreenVideoStatus::kFullscreenAndPictureInPictureDisabled);
}
}
void MediaCustomControlsFullscreenDetector::OnIntersectionChanged(
const HeapVector<Member<IntersectionObserverEntry>>& entries) {
if (!viewport_intersection_observer_ || entries.IsEmpty())
return;
auto* layout = VideoElement().GetLayoutObject();
if (!layout || entries.back()->intersectionRatio() <
kMinPossibleFullscreenIntersectionThreshold) {
// Video is not shown at all.
VideoElement().SetIsDominantVisibleContent(false);
ReportEffectivelyFullscreen(false);
return;
}
const bool is_mostly_filling_viewport =
entries.back()->intersectionRatio() >=
kMostlyFillViewportIntersectionThreshold;
VideoElement().SetIsDominantVisibleContent(is_mostly_filling_viewport);
if (!IsVideoOrParentFullscreen()) {
// The video is outside of a fullscreen element.
// This is definitely not a fullscreen video experience.
ReportEffectivelyFullscreen(false);
return;
}
if (is_mostly_filling_viewport) {
// Video takes most part (85%) of the screen, report fullscreen.
ReportEffectivelyFullscreen(true);
return;
}
const IntersectionGeometry& geometry = entries.back()->GetGeometry();
IntSize target_size = RoundedIntSize(geometry.TargetRect().size);
IntSize intersection_size = RoundedIntSize(geometry.IntersectionRect().size);
IntSize root_size = RoundedIntSize(geometry.RootRect().size);
ReportEffectivelyFullscreen(IsFullscreenVideoOfDifferentRatio(
target_size, root_size, intersection_size));
}
void MediaCustomControlsFullscreenDetector::TriggerObservation() {
if (!viewport_intersection_observer_)
return;
// Removing and re-adding the observable element is just a way to
// trigger the observation callback and reevaluate the intersection ratio.
viewport_intersection_observer_->unobserve(&VideoElement());
viewport_intersection_observer_->observe(&VideoElement());
}
bool MediaCustomControlsFullscreenDetector::IsVideoOrParentFullscreen() {
Element* fullscreen_element =
Fullscreen::FullscreenElementFrom(VideoElement().GetDocument());
if (!fullscreen_element)
return false;
return fullscreen_element->contains(&VideoElement());
}
void MediaCustomControlsFullscreenDetector::Trace(Visitor* visitor) const {
NativeEventListener::Trace(visitor);
visitor->Trace(video_element_);
visitor->Trace(viewport_intersection_observer_);
}
// static
bool MediaCustomControlsFullscreenDetector::
IsFullscreenVideoOfDifferentRatioForTesting(
const IntSize& video_size,
const IntSize& viewport_size,
const IntSize& intersection_size) {
return IsFullscreenVideoOfDifferentRatio(video_size, viewport_size,
intersection_size);
}
} // namespace blink