blob: e41dbabb02766cdc26f9ce50efff5838134cd3cc [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 <algorithm>
#include <memory>
#include <set>
#include <utility>
#include "base/compiler_specific.h"
#include "base/containers/circular_deque.h"
#include "base/feature_list.h"
#include "base/memory/raw_ptr.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_mock_time_task_runner.h"
#include "base/time/tick_clock.h"
#include "base/time/time.h"
#include "components/viz/common/features.h"
#include "components/viz/common/quads/surface_draw_quad.h"
#include "components/viz/common/quads/texture_draw_quad.h"
#include "components/viz/common/resources/transferable_resource.h"
#include "components/viz/common/surfaces/parent_local_surface_id_allocator.h"
#include "components/viz/service/display/display_resource_provider_software.h"
#include "components/viz/service/display/surface_aggregator.h"
#include "components/viz/service/frame_sinks/compositor_frame_sink_support.h"
#include "components/viz/service/frame_sinks/frame_sink_manager_impl.h"
#include "components/viz/test/compositor_frame_helpers.h"
#include "components/viz/test/fake_compositor_frame_sink_client.h"
#include "components/viz/test/surface_id_allocator_set.h"
#include "gpu/command_buffer/service/scheduler.h"
#include "gpu/command_buffer/service/shared_image/shared_image_manager.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "services/viz/public/mojom/compositing/video_detector_observer.mojom.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/gfx/geometry/rect.h"
namespace viz {
namespace {
// Implementation that just records video state changes.
class TestObserver : public mojom::VideoDetectorObserver {
public:
TestObserver() = default;
TestObserver(const TestObserver&) = delete;
TestObserver& operator=(const TestObserver&) = delete;
void Bind(mojo::PendingReceiver<mojom::VideoDetectorObserver> receiver) {
receiver_.Bind(std::move(receiver));
}
bool IsEmpty() {
receiver_.FlushForTesting();
return states_.empty();
}
void Reset() {
receiver_.FlushForTesting();
states_.clear();
}
// Pops and returns the earliest-received state.
bool PopState() {
receiver_.FlushForTesting();
CHECK(!states_.empty());
uint8_t first_state = states_.front();
states_.pop_front();
return first_state;
}
// mojom::VideoDetectorObserver implementation.
void OnVideoActivityStarted() override { states_.push_back(true); }
void OnVideoActivityEnded() override { states_.push_back(false); }
private:
// States in the order they were received.
base::circular_deque<bool> states_;
mojo::Receiver<mojom::VideoDetectorObserver> receiver_{this};
};
} // namespace
class VideoDetectorTest : public testing::Test {
public:
VideoDetectorTest()
: surface_aggregator_(frame_sink_manager_.surface_manager(),
&resource_provider_,
false) {}
VideoDetectorTest(const VideoDetectorTest&) = delete;
VideoDetectorTest& operator=(const VideoDetectorTest&) = delete;
~VideoDetectorTest() override = default;
void SetUp() override {
mock_task_runner_ = base::MakeRefCounted<base::TestMockTimeTaskRunner>(
base::Time() + base::Seconds(1), base::TimeTicks() + base::Seconds(1));
detector_ = frame_sink_manager_.CreateVideoDetectorForTesting(
mock_task_runner_->GetMockTickClock(), mock_task_runner_);
mojo::PendingRemote<mojom::VideoDetectorObserver> video_detector_observer;
observer_.Bind(video_detector_observer.InitWithNewPipeAndPassReceiver());
detector_->AddObserver(std::move(video_detector_observer));
root_frame_sink_ = CreateFrameSink();
ParentLocalSurfaceIdAllocator* allocator =
allocators_.GetAllocator(root_frame_sink_->frame_sink_id());
allocator->GenerateId();
root_frame_sink_->SubmitCompositorFrame(
allocator->GetCurrentLocalSurfaceId(), MakeDefaultCompositorFrame());
}
protected:
// Constants placed here for convenience.
static constexpr int kMinFps = VideoDetector::kMinFramesPerSecond;
static constexpr gfx::Rect kMinRect =
gfx::Rect(VideoDetector::kMinDamageWidth,
VideoDetector::kMinDamageHeight);
static constexpr base::TimeDelta kMinDuration =
VideoDetector::kMinVideoDuration;
static constexpr base::TimeDelta kTimeout = VideoDetector::kMaxVideoTimeout;
// Move |detector_|'s idea of the current time forward by |delta|.
void AdvanceTime(base::TimeDelta delta) {
mock_task_runner_->FastForwardBy(delta);
}
void CreateDisplayFrame() {
surface_aggregator_.Aggregate(root_frame_sink_->last_activated_surface_id(),
mock_task_runner_->NowTicks(),
gfx::OVERLAY_TRANSFORM_NONE);
}
void EmbedClient(CompositorFrameSinkSupport* frame_sink) {
embedded_clients_.insert(frame_sink);
SubmitRootFrame();
}
void SubmitRootFrame() {
CompositorFrame frame = MakeDefaultCompositorFrame();
CompositorRenderPass* render_pass = frame.render_pass_list.back().get();
SharedQuadState* shared_quad_state =
render_pass->CreateAndAppendSharedQuadState();
for (CompositorFrameSinkSupport* frame_sink : embedded_clients_) {
SurfaceDrawQuad* quad =
render_pass->CreateAndAppendDrawQuad<SurfaceDrawQuad>();
quad->SetNew(
shared_quad_state, gfx::Rect(0, 0, 10, 10), gfx::Rect(0, 0, 5, 5),
SurfaceRange(std::nullopt, frame_sink->last_activated_surface_id()),
SkColors::kMagenta, /*stretch_content_to_fill_bounds=*/false);
}
root_frame_sink_->SubmitCompositorFrame(
root_frame_sink_->last_activated_local_surface_id(), std::move(frame));
}
void SendUpdate(CompositorFrameSinkSupport* frame_sink,
const gfx::Rect& damage,
bool may_contain_video,
bool use_per_quad_damage) {
LocalSurfaceId local_surface_id =
frame_sink->last_activated_local_surface_id();
if (!local_surface_id.is_valid()) {
ParentLocalSurfaceIdAllocator* allocator =
allocators_.GetAllocator(frame_sink->frame_sink_id());
allocator->GenerateId();
local_surface_id = allocator->GetCurrentLocalSurfaceId();
}
frame_sink->SubmitCompositorFrame(
local_surface_id,
use_per_quad_damage
? MakeDamagedCompositorFrameWithPerQuadDamage(damage,
may_contain_video)
: MakeDamagedCompositorFrame(damage, may_contain_video));
}
// Report updates to |client| of area |damage| at a rate of
// |updates_per_second| over |duration|. The first update will be sent
// immediately and time will have advanced by |duration| upon returning.
void SendUpdates(CompositorFrameSinkSupport* frame_sink,
const gfx::Rect& damage,
bool may_contain_video,
bool use_per_quad_damage,
int updates_per_second,
base::TimeDelta duration) {
const base::TimeDelta time_between_updates =
base::Seconds(1.0 / updates_per_second);
for (base::TimeDelta d; d < duration; d += time_between_updates) {
SendUpdate(frame_sink, damage, may_contain_video, use_per_quad_damage);
CreateDisplayFrame();
AdvanceTime(std::min(time_between_updates, duration - d));
}
}
std::unique_ptr<CompositorFrameSinkSupport> CreateFrameSink() {
constexpr bool is_root = false;
static uint32_t client_id = 1;
FrameSinkId frame_sink_id(client_id++, 0);
frame_sink_manager_.RegisterFrameSinkId(frame_sink_id,
true /* report_activation */);
auto frame_sink = std::make_unique<CompositorFrameSinkSupport>(
&frame_sink_client_, &frame_sink_manager_, frame_sink_id, is_root);
SendUpdate(frame_sink.get(), gfx::Rect(), /*may_contain_video*/ false,
/*use_per_quad_damage*/ false);
return frame_sink;
}
TestObserver observer_;
scoped_refptr<base::TestMockTimeTaskRunner> mock_task_runner_;
protected:
CompositorFrame MakeDamagedCompositorFrame(const gfx::Rect& damage,
bool may_contain_video) {
constexpr gfx::Rect kFrameSinkRect(10000, 10000);
auto frame =
CompositorFrameBuilder().AddRenderPass(kFrameSinkRect, damage).Build();
frame.metadata.may_contain_video = may_contain_video;
return frame;
}
CompositorFrame MakeDamagedCompositorFrameWithPerQuadDamage(
const gfx::Rect& damage,
bool may_contain_video) {
constexpr gfx::Rect kFrameSinkRect(10000, 10000);
auto frame =
CompositorFrameBuilder()
.AddRenderPass(RenderPassBuilder(kFrameSinkRect)
.AddTextureQuad(kFrameSinkRect, ResourceId(1234))
.SetQuadDamageRect(kFrameSinkRect))
.PopulateResources()
.Build();
frame.metadata.may_contain_video = may_contain_video;
return frame;
}
base::test::ScopedFeatureList scoped_feature_list_;
gpu::SharedImageManager shared_image_manager_;
gpu::SyncPointManager sync_point_manager_;
gpu::Scheduler gpu_scheduler_{&sync_point_manager_};
FrameSinkManagerImpl frame_sink_manager_{FrameSinkManagerImpl::InitParams()};
DisplayResourceProviderSoftware resource_provider_{&shared_image_manager_,
&gpu_scheduler_};
FakeCompositorFrameSinkClient frame_sink_client_;
SurfaceIdAllocatorSet allocators_;
SurfaceAggregator surface_aggregator_;
std::unique_ptr<CompositorFrameSinkSupport> root_frame_sink_;
std::set<raw_ptr<CompositorFrameSinkSupport, SetExperimental>>
embedded_clients_;
raw_ptr<VideoDetector> detector_;
};
constexpr gfx::Rect VideoDetectorTest::kMinRect;
constexpr base::TimeDelta VideoDetectorTest::kMinDuration;
constexpr base::TimeDelta VideoDetectorTest::kTimeout;
// Verify that VideoDetector does not report clients with small damage rects.
TEST_F(VideoDetectorTest, DontReportWhenDamageTooSmall) {
std::unique_ptr<CompositorFrameSinkSupport> frame_sink = CreateFrameSink();
EmbedClient(frame_sink.get());
{
// Send damages with a smaller width than |kMinRect|. Make sure video
// activity isn't detected.
gfx::Rect rect = kMinRect;
rect.Inset(gfx::Insets::TLBR(0, 0, 0, 1));
SendUpdates(frame_sink.get(), rect, /*may_contain_video=*/true,
/*use_per_quad_damage=*/false, 2 * kMinFps, 2 * kMinDuration);
EXPECT_TRUE(observer_.IsEmpty());
}
{
// Send damages with a smaller height than |kMinRect|. Make sure video
// activity isn't detected.
gfx::Rect rect = kMinRect;
rect.Inset(gfx::Insets::TLBR(0, 0, 0, 1));
SendUpdates(frame_sink.get(), rect, /*may_contain_video=*/true,
/*use_per_quad_damage=*/false, 2 * kMinFps, 2 * kMinDuration);
EXPECT_TRUE(observer_.IsEmpty());
}
}
// Verify that VideoDetector does not report clients with a low frame rate.
TEST_F(VideoDetectorTest, DontReportWhenFramerateTooLow) {
std::unique_ptr<CompositorFrameSinkSupport> frame_sink = CreateFrameSink();
EmbedClient(frame_sink.get());
SendUpdates(frame_sink.get(), kMinRect, /*may_contain_video=*/true,
/*use_per_quad_damage=*/false, kMinFps - 5, 2 * kMinDuration);
EXPECT_TRUE(observer_.IsEmpty());
}
// Verify that VideoDetector does not report clients until they have played for
// the minimum necessary duration.
TEST_F(VideoDetectorTest, DontReportWhenNotPlayingLongEnough) {
std::unique_ptr<CompositorFrameSinkSupport> frame_sink = CreateFrameSink();
EmbedClient(frame_sink.get());
SendUpdates(frame_sink.get(), kMinRect, /*may_contain_video=*/true,
/*use_per_quad_damage=*/false, 2 * kMinFps, 0.5 * kMinDuration);
EXPECT_TRUE(observer_.IsEmpty());
SendUpdates(frame_sink.get(), kMinRect, /*may_contain_video=*/true,
/*use_per_quad_damage=*/false, 2 * kMinFps, 0.6 * kMinDuration);
EXPECT_TRUE(observer_.PopState());
EXPECT_TRUE(observer_.IsEmpty());
}
// Verify that VideoDetector does not report clients that are not visible
// on screen.
TEST_F(VideoDetectorTest, DontReportWhenClientHidden) {
std::unique_ptr<CompositorFrameSinkSupport> frame_sink = CreateFrameSink();
SendUpdates(frame_sink.get(), kMinRect, /*may_contain_video=*/true,
/*use_per_quad_damage=*/false, kMinFps + 5, 2 * kMinDuration);
EXPECT_TRUE(observer_.IsEmpty());
// Make the client visible.
observer_.Reset();
AdvanceTime(kTimeout);
EmbedClient(frame_sink.get());
SendUpdates(frame_sink.get(), kMinRect, /*may_contain_video=*/true,
/*use_per_quad_damage=*/false, kMinFps + 5, 2 * kMinDuration);
EXPECT_TRUE(observer_.PopState());
EXPECT_TRUE(observer_.IsEmpty());
}
TEST_F(VideoDetectorTest, DoesNotReportNonVideoFrames) {
const base::TimeDelta kDuration = kMinDuration + base::Milliseconds(100);
std::unique_ptr<CompositorFrameSinkSupport> frame_sink = CreateFrameSink();
EmbedClient(frame_sink.get());
SendUpdates(frame_sink.get(), kMinRect, /*may_contain_video=*/false,
/*use_per_quad_damage=*/false, kMinFps + 5, kDuration);
EXPECT_TRUE(observer_.IsEmpty());
}
// Turn video activity on and off. Make sure the observers are notified
// properly.
TEST_F(VideoDetectorTest, ReportStartAndStop) {
const base::TimeDelta kDuration = kMinDuration + base::Milliseconds(100);
std::unique_ptr<CompositorFrameSinkSupport> frame_sink = CreateFrameSink();
EmbedClient(frame_sink.get());
SendUpdates(frame_sink.get(), kMinRect, /*may_contain_video=*/true,
/*use_per_quad_damage=*/false, kMinFps + 5, kDuration);
EXPECT_TRUE(observer_.PopState());
EXPECT_TRUE(observer_.IsEmpty());
AdvanceTime(kTimeout);
EXPECT_FALSE(observer_.PopState());
EXPECT_TRUE(observer_.IsEmpty());
// Start playing again.
SendUpdates(frame_sink.get(), kMinRect, /*may_contain_video=*/true,
/*use_per_quad_damage=*/false, kMinFps + 5, kDuration);
EXPECT_TRUE(observer_.PopState());
EXPECT_TRUE(observer_.IsEmpty());
AdvanceTime(kTimeout);
EXPECT_FALSE(observer_.PopState());
EXPECT_TRUE(observer_.IsEmpty());
}
// If there are multiple clients playing video, make sure that observers only
// receive a single notification.
TEST_F(VideoDetectorTest, ReportOnceForMultipleClients) {
std::unique_ptr<CompositorFrameSinkSupport> frame_sink1 = CreateFrameSink();
std::unique_ptr<CompositorFrameSinkSupport> frame_sink2 = CreateFrameSink();
EmbedClient(frame_sink1.get());
EmbedClient(frame_sink2.get());
// Even if there's video playing in both clients, the observer should only
// receive a single notification.
constexpr int fps = 2 * kMinFps;
constexpr base::TimeDelta time_between_updates = base::Seconds(1.0 / fps);
for (base::TimeDelta d; d < 2 * kMinDuration; d += time_between_updates) {
SendUpdate(frame_sink1.get(), kMinRect, /*may_contain_video=*/true,
/*use_per_quad_damage=*/false);
SendUpdate(frame_sink2.get(), kMinRect, /*may_contain_video=*/true,
/*use_per_quad_damage=*/false);
AdvanceTime(time_between_updates);
CreateDisplayFrame();
}
EXPECT_TRUE(observer_.PopState());
EXPECT_TRUE(observer_.IsEmpty());
}
TEST_F(VideoDetectorTest, ReportBasedOnPerQuadDamage) {
const base::TimeDelta kDuration = kMinDuration + base::Milliseconds(100);
std::unique_ptr<CompositorFrameSinkSupport> frame_sink = CreateFrameSink();
EmbedClient(frame_sink.get());
SendUpdates(frame_sink.get(), kMinRect, /*may_contain_video=*/true,
/*use_per_quad_damage=*/true, kMinFps + 5, kDuration);
EXPECT_TRUE(observer_.PopState());
EXPECT_TRUE(observer_.IsEmpty());
AdvanceTime(kTimeout);
EXPECT_FALSE(observer_.PopState());
EXPECT_TRUE(observer_.IsEmpty());
}
} // namespace viz