blob: a516e51f3c36abdddc1d28d0e53b1c6f85e92da0 [file] [log] [blame]
// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/viz/service/frame_sinks/video_detector.h"
#include <array>
#include <memory>
#include <utility>
#include <vector>
#include "base/task/sequenced_task_runner.h"
#include "base/time/time.h"
#include "components/viz/common/features.h"
#include "components/viz/common/quads/compositor_frame.h"
#include "components/viz/common/quads/draw_quad.h"
#include "components/viz/common/quads/texture_draw_quad.h"
#include "components/viz/service/surfaces/surface.h"
#include "components/viz/service/surfaces/surface_manager.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rect_conversions.h"
namespace viz {
constexpr base::TimeDelta VideoDetector::kMaxVideoTimeout;
constexpr base::TimeDelta VideoDetector::kMinVideoTimeout;
constexpr base::TimeDelta VideoDetector::kMinVideoDuration;
// Stores information about updates to a client and determines whether it's
// likely that a video is playing in it.
class VideoDetector::ClientInfo {
public:
ClientInfo() = default;
ClientInfo(const ClientInfo&) = delete;
ClientInfo& operator=(const ClientInfo&) = delete;
// Called when a Surface belonging to this client is drawn. Returns true if we
// determine that video is playing in this client.
bool ReportDrawnAndCheckForVideo(Surface* surface, base::TimeTicks now) {
uint32_t frame_index = surface->GetActiveFrameIndex();
// If |frame_index| hasn't increased, then no new frame was submitted since
// the last draw.
if (frame_index <= last_drawn_frame_index_)
return false;
last_drawn_frame_index_ = frame_index;
const CompositorFrame& frame = surface->GetActiveFrame();
if (!frame.metadata.may_contain_video) {
return false;
}
gfx::Rect damage = frame.render_pass_list.back()->damage_rect;
if (frame.render_pass_list.back()->has_per_quad_damage) {
for (auto* quad : frame.render_pass_list.back()->quad_list) {
if (quad->material != DrawQuad::Material::kTextureContent)
continue;
auto* texture_quad = TextureDrawQuad::MaterialCast(quad);
if (!texture_quad->damage_rect)
continue;
damage.Union(*texture_quad->damage_rect);
}
}
damage =
gfx::ScaleToEnclosingRect(damage, 1.f / frame.device_scale_factor());
if (damage.width() < kMinDamageWidth || damage.height() < kMinDamageHeight)
return false;
// If the buffer is full, drop the first timestamp.
if (buffer_size_ == kMinFramesPerSecond) {
buffer_start_ = (buffer_start_ + 1) % kMinFramesPerSecond;
buffer_size_--;
}
update_times_[(buffer_start_ + buffer_size_) % kMinFramesPerSecond] = now;
buffer_size_++;
const bool in_video =
(buffer_size_ == kMinFramesPerSecond) &&
(now - update_times_[buffer_start_] <= base::Seconds(1));
if (in_video && video_start_time_.is_null())
video_start_time_ = update_times_[buffer_start_];
else if (!in_video && !video_start_time_.is_null())
video_start_time_ = base::TimeTicks();
const base::TimeDelta elapsed = now - video_start_time_;
return in_video && elapsed >= kMinVideoDuration;
}
private:
// Circular buffer containing update times of the last (up to
// |kMinFramesPerSecond|) video-sized updates to this client.
std::array<base::TimeTicks, kMinFramesPerSecond> update_times_;
// Time at which the current sequence of updates that looks like video
// started. Empty if video isn't currently playing.
base::TimeTicks video_start_time_;
// Index into |update_times_| of the oldest update.
uint32_t buffer_start_ = 0;
// Number of updates stored in |update_times_|.
uint32_t buffer_size_ = 0;
// Frame index of the last drawn Surface. We use this number to determine
// whether a new frame was submitted since the last time the Surface was
// drawn.
uint32_t last_drawn_frame_index_ = 0;
};
VideoDetector::VideoDetector(
const std::vector<FrameSinkId>& registered_frame_sink_ids,
SurfaceManager* surface_manager,
const base::TickClock* tick_clock,
scoped_refptr<base::SequencedTaskRunner> task_runner)
: tick_clock_(tick_clock),
video_inactive_timer_(tick_clock),
surface_manager_(surface_manager) {
surface_manager_->AddObserver(this);
for (auto& frame_sink_id : registered_frame_sink_ids)
client_infos_[frame_sink_id] = std::make_unique<ClientInfo>();
if (task_runner)
video_inactive_timer_.SetTaskRunner(task_runner);
}
VideoDetector::~VideoDetector() {
surface_manager_->RemoveObserver(this);
}
void VideoDetector::OnVideoActivityEnded() {
DCHECK(video_is_playing_);
video_is_playing_ = false;
for (auto& observer : observers_) {
observer->OnVideoActivityEnded();
}
}
void VideoDetector::AddObserver(
mojo::PendingRemote<mojom::VideoDetectorObserver> pending_observer) {
mojo::Remote<mojom::VideoDetectorObserver> observer(
std::move(pending_observer));
if (video_is_playing_)
observer->OnVideoActivityStarted();
observers_.Add(std::move(observer));
}
void VideoDetector::OnFrameSinkIdRegistered(const FrameSinkId& frame_sink_id) {
DCHECK(!client_infos_.count(frame_sink_id));
client_infos_[frame_sink_id] = std::make_unique<ClientInfo>();
}
void VideoDetector::OnFrameSinkIdInvalidated(const FrameSinkId& frame_sink_id) {
client_infos_.erase(frame_sink_id);
}
bool VideoDetector::OnSurfaceDamaged(const SurfaceId& surface_id,
const BeginFrameAck& ack,
HandleInteraction handle_interaction) {
return false;
}
// |surface| is scheduled to be drawn. See if it has a new frame since the
// last time it was drawn and record the damage.
void VideoDetector::OnSurfaceWillBeDrawn(Surface* surface) {
// If there is no observer, don't waste cycles detecting video activity.
if (observers_.empty())
return;
const FrameSinkId& frame_sink_id = surface->surface_id().frame_sink_id();
auto it = client_infos_.find(frame_sink_id);
// If the corresponding entry in |client_infos_| does not exist, it means the
// FrameSinkId has been invalidated and the client's CompositorFrameSink is
// destroyed so it cannot send new frames and video activity is impossible.
if (it == client_infos_.end())
return;
base::TimeTicks now = tick_clock_->NowTicks();
if (it->second->ReportDrawnAndCheckForVideo(surface, now)) {
// Avoid (re)starting the timer every frame since it has considerable
// overhead.
if (!video_inactive_timer_.IsRunning() ||
(video_inactive_timer_.desired_run_time() - now) < kMinVideoTimeout) {
video_inactive_timer_.Start(FROM_HERE, kMaxVideoTimeout, this,
&VideoDetector::OnVideoActivityEnded);
}
if (!video_is_playing_) {
video_is_playing_ = true;
for (auto& observer : observers_) {
observer->OnVideoActivityStarted();
}
}
}
}
} // namespace viz