blob: 3ff0de7ef497348f829aa5e34c67f70c4aa76032 [file] [log] [blame]
// Copyright 2013 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 "chrome/browser/media/webrtc/native_desktop_media_list.h"
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#include <utility>
#include <vector>
#include "base/location.h"
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/strings/utf_string_conversions.h"
#include "base/synchronization/lock.h"
#include "base/task/single_thread_task_runner.h"
#include "base/threading/thread_task_runner_handle.h"
#include "build/build_config.h"
#include "chrome/browser/media/webrtc/desktop_media_list.h"
#include "chrome/test/views/chrome_views_test_base.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/webrtc/modules/desktop_capture/desktop_capturer.h"
#include "third_party/webrtc/modules/desktop_capture/desktop_frame.h"
#include "ui/views/widget/widget.h"
#if defined(USE_AURA)
#include "ui/aura/window.h"
#include "ui/aura/window_tree_host.h"
#include "ui/views/widget/desktop_aura/desktop_native_widget_aura.h"
#endif
using content::DesktopMediaID;
using testing::_;
using testing::DoAll;
namespace {
// Aura window capture unit tests are not stable. crbug.com/602494 and
// crbug.com/603823.
// #define ENABLE_AURA_WINDOW_TESTS
static const int kDefaultWindowCount = 2;
#if defined(ENABLE_AURA_WINDOW_TESTS)
static const int kDefaultAuraCount = 1;
#else
static const int kDefaultAuraCount = 0;
#endif
// Returns the given index, offset by a fixed value such that it does not
// collide with Aura window IDs. Intended for usage with indices that are passed
// to AddNativeWindow().
int WindowIndex(int index) {
// During test setup, an Aura window is created. On some platforms, e.g.
// Wayland, this window's ID starts at 1 for the first run test, then 2 for
// the second test, etc. To avoid clashes between this Aura window's ID and
// the ID of native windows, this offset is added to every index passed to
// AddNativeWindow(). Its value has no significance, it is just an arbitrary,
// large number.
static constexpr int kWindowIndexOffset = 1 << 12;
return index + kWindowIndexOffset;
}
class MockObserver : public DesktopMediaListObserver {
public:
MOCK_METHOD1(OnSourceAdded, void(int index));
MOCK_METHOD1(OnSourceRemoved, void(int index));
MOCK_METHOD2(OnSourceMoved, void(int old_index, int new_index));
MOCK_METHOD1(OnSourceNameChanged, void(int index));
MOCK_METHOD1(OnSourceThumbnailChanged, void(int index));
MOCK_METHOD1(OnSourcePreviewChanged, void(size_t index));
};
class FakeScreenCapturer : public webrtc::DesktopCapturer {
public:
FakeScreenCapturer() {}
FakeScreenCapturer(const FakeScreenCapturer&) = delete;
FakeScreenCapturer& operator=(const FakeScreenCapturer&) = delete;
~FakeScreenCapturer() override {}
// webrtc::ScreenCapturer implementation.
void Start(Callback* callback) override { callback_ = callback; }
void CaptureFrame() override {
DCHECK(callback_);
std::unique_ptr<webrtc::DesktopFrame> frame(
new webrtc::BasicDesktopFrame(webrtc::DesktopSize(10, 10)));
callback_->OnCaptureResult(webrtc::DesktopCapturer::Result::SUCCESS,
std::move(frame));
}
bool GetSourceList(SourceList* screens) override {
screens->push_back({0});
return true;
}
bool SelectSource(SourceId id) override {
EXPECT_EQ(0, id);
return true;
}
protected:
raw_ptr<Callback> callback_;
};
class FakeWindowCapturer : public webrtc::DesktopCapturer {
public:
FakeWindowCapturer() : callback_(nullptr) {}
FakeWindowCapturer(const FakeWindowCapturer&) = delete;
FakeWindowCapturer& operator=(const FakeWindowCapturer&) = delete;
~FakeWindowCapturer() override {}
void SetWindowList(const SourceList& list) {
base::AutoLock lock(window_list_lock_);
window_list_ = list;
}
// Sets |value| thats going to be used to memset() content of the frames
// generated for |window_id|. By default generated frames are set to zeros.
void SetNextFrameValue(SourceId window_id, int8_t value) {
base::AutoLock lock(frame_values_lock_);
frame_values_[window_id] = value;
}
// webrtc::WindowCapturer implementation.
void Start(Callback* callback) override { callback_ = callback; }
void CaptureFrame() override {
DCHECK(callback_);
base::AutoLock lock(frame_values_lock_);
auto it = frame_values_.find(selected_window_id_);
int8_t value = (it != frame_values_.end()) ? it->second : 0;
std::unique_ptr<webrtc::DesktopFrame> frame(
new webrtc::BasicDesktopFrame(webrtc::DesktopSize(10, 10)));
memset(frame->data(), value, frame->stride() * frame->size().height());
callback_->OnCaptureResult(webrtc::DesktopCapturer::Result::SUCCESS,
std::move(frame));
}
bool GetSourceList(SourceList* windows) override {
base::AutoLock lock(window_list_lock_);
*windows = window_list_;
return true;
}
bool SelectSource(SourceId id) override {
selected_window_id_ = id;
return true;
}
bool FocusOnSelectedSource() override { return true; }
private:
raw_ptr<Callback> callback_;
SourceList window_list_;
base::Lock window_list_lock_;
SourceId selected_window_id_;
// Frames to be captured per window.
std::map<SourceId, int8_t> frame_values_;
base::Lock frame_values_lock_;
};
} // namespace
ACTION_P2(CheckListSize, model, expected_list_size) {
EXPECT_EQ(expected_list_size, model->GetSourceCount());
}
ACTION_P2(QuitRunLoop, task_runner, run_loop) {
task_runner->PostTask(FROM_HERE, run_loop->QuitWhenIdleClosure());
}
class NativeDesktopMediaListTest : public ChromeViewsTestBase {
public:
NativeDesktopMediaListTest() = default;
NativeDesktopMediaListTest(const NativeDesktopMediaListTest&) = delete;
NativeDesktopMediaListTest& operator=(const NativeDesktopMediaListTest&) =
delete;
void TearDown() override {
for (size_t i = 0; i < desktop_widgets_.size(); i++)
desktop_widgets_[i].reset();
ChromeViewsTestBase::TearDown();
}
void AddNativeWindow(int id) {
webrtc::DesktopCapturer::Source window;
window.id = id;
window.title = "Test window";
window_list_.push_back(window);
}
#if defined(USE_AURA)
std::unique_ptr<views::Widget> CreateDesktopWidget() {
std::unique_ptr<views::Widget> widget(new views::Widget);
views::Widget::InitParams params;
params.type = views::Widget::InitParams::TYPE_WINDOW_FRAMELESS;
params.accept_events = false;
params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
params.native_widget = new views::DesktopNativeWidgetAura(widget.get());
params.bounds = gfx::Rect(0, 0, 20, 20);
widget->Init(std::move(params));
widget->Show();
return widget;
}
void AddAuraWindow() {
webrtc::DesktopCapturer::Source window;
window.title = "Test window";
// Create a aura native window through a widget.
desktop_widgets_.push_back(CreateDesktopWidget());
aura::WindowTreeHost* const host =
desktop_widgets_.back()->GetNativeWindow()->GetHost();
aura::Window* const aura_window = host->window();
// Get the native window's id.
gfx::AcceleratedWidget widget = host->GetAcceleratedWidget();
#if BUILDFLAG(IS_WIN)
window.id = reinterpret_cast<DesktopMediaID::Id>(widget);
#else
window.id = widget;
#endif
// Get the aura window's id.
DesktopMediaID aura_id = DesktopMediaID::RegisterNativeWindow(
DesktopMediaID::TYPE_WINDOW, aura_window);
native_aura_id_map_[window.id] = aura_id.window_id;
window_list_.push_back(window);
}
void RemoveAuraWindow(int index) {
DCHECK_LT(index, static_cast<int>(desktop_widgets_.size()));
// Get the native window's id.
aura::Window* aura_window = desktop_widgets_[index]->GetNativeWindow();
gfx::AcceleratedWidget widget =
aura_window->GetHost()->GetAcceleratedWidget();
#if BUILDFLAG(IS_WIN)
int native_id = reinterpret_cast<DesktopMediaID::Id>(widget);
#else
int native_id = widget;
#endif
// Remove the widget and associated aura window.
desktop_widgets_.erase(desktop_widgets_.begin() + index);
// Remove the aura window from the window list.
size_t i;
for (i = 0; i < window_list_.size(); i++) {
if (window_list_[i].id == native_id)
break;
}
DCHECK_LT(i, window_list_.size());
window_list_.erase(window_list_.begin() + i);
native_aura_id_map_.erase(native_id);
}
#endif // defined(USE_AURA)
void AddWindowsAndVerify(bool has_view_dialog) {
window_capturer_ = new FakeWindowCapturer();
model_ = std::make_unique<NativeDesktopMediaList>(
DesktopMediaList::Type::kWindow,
base::WrapUnique(window_capturer_.get()));
// Set update period to reduce the time it takes to run tests.
model_->SetUpdatePeriod(base::Milliseconds(20));
// Set up windows.
size_t aura_window_first_index = kDefaultWindowCount - kDefaultAuraCount;
for (size_t i = 0; i < kDefaultWindowCount; ++i) {
if (i < aura_window_first_index) {
AddNativeWindow(WindowIndex(i));
} else {
#if defined(USE_AURA)
AddAuraWindow();
#endif
}
}
if (window_capturer_)
window_capturer_->SetWindowList(window_list_);
size_t window_count = kDefaultWindowCount;
// Set view dialog window ID as the first window id.
if (has_view_dialog) {
DesktopMediaID dialog_window_id(DesktopMediaID::TYPE_WINDOW,
window_list_[0].id);
model_->SetViewDialogWindowId(dialog_window_id);
window_count--;
aura_window_first_index--;
}
base::RunLoop run_loop;
{
testing::InSequence dummy;
for (size_t i = 0; i < window_count; ++i) {
EXPECT_CALL(observer_, OnSourceAdded(i))
.WillOnce(CheckListSize(model_.get(), static_cast<int>(i + 1)));
}
for (size_t i = 0; i < window_count - 1; ++i) {
EXPECT_CALL(observer_, OnSourceThumbnailChanged(i));
}
EXPECT_CALL(observer_, OnSourceThumbnailChanged(window_count - 1))
.WillOnce(
QuitRunLoop(base::ThreadTaskRunnerHandle::Get(), &run_loop));
}
model_->StartUpdating(&observer_);
run_loop.Run();
for (size_t i = 0; i < window_count; ++i) {
EXPECT_EQ(model_->GetSource(i).id.type, DesktopMediaID::TYPE_WINDOW);
EXPECT_EQ(model_->GetSource(i).name, u"Test window");
int index = has_view_dialog ? i + 1 : i;
int native_id = window_list_[index].id;
EXPECT_EQ(model_->GetSource(i).id.id, native_id);
#if defined(USE_AURA)
if (i >= aura_window_first_index)
EXPECT_EQ(model_->GetSource(i).id.window_id,
native_aura_id_map_[native_id]);
#endif
}
testing::Mock::VerifyAndClearExpectations(&observer_);
}
protected:
// Must be listed before |model_|, so it's destroyed last.
MockObserver observer_;
// Owned by |model_|;
raw_ptr<FakeWindowCapturer> window_capturer_;
webrtc::DesktopCapturer::SourceList window_list_;
std::vector<std::unique_ptr<views::Widget>> desktop_widgets_;
std::map<DesktopMediaID::Id, DesktopMediaID::Id> native_aura_id_map_;
std::unique_ptr<NativeDesktopMediaList> model_;
};
TEST_F(NativeDesktopMediaListTest, Windows) {
AddWindowsAndVerify(false);
}
TEST_F(NativeDesktopMediaListTest, ScreenOnly) {
model_ = std::make_unique<NativeDesktopMediaList>(
DesktopMediaList::Type::kScreen, std::make_unique<FakeScreenCapturer>());
// Set update period to reduce the time it takes to run tests.
model_->SetUpdatePeriod(base::Milliseconds(20));
base::RunLoop run_loop;
{
testing::InSequence dummy;
EXPECT_CALL(observer_, OnSourceAdded(0))
.WillOnce(CheckListSize(model_.get(), 1));
EXPECT_CALL(observer_, OnSourceThumbnailChanged(0))
.WillOnce(QuitRunLoop(base::ThreadTaskRunnerHandle::Get(), &run_loop));
}
model_->StartUpdating(&observer_);
run_loop.Run();
EXPECT_EQ(model_->GetSource(0).id.type, DesktopMediaID::TYPE_SCREEN);
EXPECT_EQ(model_->GetSource(0).id.id, 0);
}
// Verifies that the window specified with SetViewDialogWindowId() is filtered
// from the results.
TEST_F(NativeDesktopMediaListTest, WindowFiltering) {
AddWindowsAndVerify(true);
}
TEST_F(NativeDesktopMediaListTest, AddNativeWindow) {
AddWindowsAndVerify(false);
base::RunLoop run_loop;
const int index = kDefaultWindowCount;
EXPECT_CALL(observer_, OnSourceAdded(index))
.WillOnce(
DoAll(CheckListSize(model_.get(), kDefaultWindowCount + 1),
QuitRunLoop(base::ThreadTaskRunnerHandle::Get(), &run_loop)));
AddNativeWindow(WindowIndex(index));
window_capturer_->SetWindowList(window_list_);
run_loop.Run();
EXPECT_EQ(model_->GetSource(index).id.type, DesktopMediaID::TYPE_WINDOW);
EXPECT_EQ(model_->GetSource(index).id.id, WindowIndex(index));
}
#if defined(ENABLE_AURA_WINDOW_TESTS)
TEST_F(NativeDesktopMediaListTest, AddAuraWindow) {
AddWindowsAndVerify(false);
base::RunLoop run_loop;
const int index = kDefaultWindowCount;
EXPECT_CALL(observer_, OnSourceAdded(index))
.WillOnce(
DoAll(CheckListSize(kDefaultWindowCount + 1),
QuitRunLoop(base::ThreadTaskRunnerHandle::Get(), &run_loop)));
AddAuraWindow();
window_capturer_->SetWindowList(window_list_);
run_loop.Run();
int native_id = window_list_.back().id;
EXPECT_EQ(model_->GetSource(index).id.type, DesktopMediaID::TYPE_WINDOW);
EXPECT_EQ(model_->GetSource(index).id.id, native_id);
EXPECT_EQ(model_->GetSource(index).id.window_id,
native_aura_id_map_[native_id]);
}
#endif // defined(ENABLE_AURA_WINDOW_TESTS)
TEST_F(NativeDesktopMediaListTest, RemoveNativeWindow) {
AddWindowsAndVerify(false);
base::RunLoop run_loop;
EXPECT_CALL(observer_, OnSourceRemoved(0))
.WillOnce(
DoAll(CheckListSize(model_.get(), kDefaultWindowCount - 1),
QuitRunLoop(base::ThreadTaskRunnerHandle::Get(), &run_loop)));
window_list_.erase(window_list_.begin());
window_capturer_->SetWindowList(window_list_);
run_loop.Run();
}
#if defined(ENABLE_AURA_WINDOW_TESTS)
TEST_F(NativeDesktopMediaListTest, RemoveAuraWindow) {
AddWindowsAndVerify(false);
base::RunLoop run_loop;
int aura_window_start_index = kDefaultWindowCount - kDefaultAuraCount;
EXPECT_CALL(observer_, OnSourceRemoved(aura_window_start_index))
.WillOnce(
DoAll(CheckListSize(model_.get(), kDefaultWindowCount - 1),
QuitRunLoop(base::ThreadTaskRunnerHandle::Get(), &run_loop)));
RemoveAuraWindow(0);
window_capturer_->SetWindowList(window_list_);
run_loop.Run();
}
#endif // defined(ENABLE_AURA_WINDOW_TESTS)
TEST_F(NativeDesktopMediaListTest, RemoveAllWindows) {
AddWindowsAndVerify(false);
base::RunLoop run_loop;
testing::InSequence seq;
for (int i = 0; i < kDefaultWindowCount - 1; i++) {
EXPECT_CALL(observer_, OnSourceRemoved(0))
.WillOnce(CheckListSize(model_.get(), kDefaultWindowCount - i - 1));
}
EXPECT_CALL(observer_, OnSourceRemoved(0))
.WillOnce(
DoAll(CheckListSize(model_.get(), 0),
QuitRunLoop(base::ThreadTaskRunnerHandle::Get(), &run_loop)));
window_list_.clear();
window_capturer_->SetWindowList(window_list_);
run_loop.Run();
}
TEST_F(NativeDesktopMediaListTest, UpdateTitle) {
AddWindowsAndVerify(false);
base::RunLoop run_loop;
EXPECT_CALL(observer_, OnSourceNameChanged(0))
.WillOnce(QuitRunLoop(base::ThreadTaskRunnerHandle::Get(), &run_loop));
const std::string kTestTitle = "New Title";
window_list_[0].title = kTestTitle;
window_capturer_->SetWindowList(window_list_);
run_loop.Run();
EXPECT_EQ(model_->GetSource(0).name, base::UTF8ToUTF16(kTestTitle));
}
TEST_F(NativeDesktopMediaListTest, UpdateThumbnail) {
AddWindowsAndVerify(false);
// Aura windows' thumbnails may unpredictably change over time.
for (size_t i = kDefaultWindowCount - kDefaultAuraCount;
i < kDefaultWindowCount; ++i) {
EXPECT_CALL(observer_, OnSourceThumbnailChanged(i))
.Times(testing::AnyNumber());
}
base::RunLoop run_loop;
EXPECT_CALL(observer_, OnSourceThumbnailChanged(0))
.WillOnce(QuitRunLoop(base::ThreadTaskRunnerHandle::Get(), &run_loop));
// Update frame for the window and verify that we get notification about it.
window_capturer_->SetNextFrameValue(WindowIndex(0), 10);
run_loop.Run();
}
TEST_F(NativeDesktopMediaListTest, MoveWindow) {
AddWindowsAndVerify(false);
base::RunLoop run_loop;
EXPECT_CALL(observer_, OnSourceMoved(1, 0))
.WillOnce(
DoAll(CheckListSize(model_.get(), kDefaultWindowCount),
QuitRunLoop(base::ThreadTaskRunnerHandle::Get(), &run_loop)));
std::swap(window_list_[0], window_list_[1]);
window_capturer_->SetWindowList(window_list_);
run_loop.Run();
}
// This test verifies that webrtc::DesktopCapturer::CaptureFrame() is not
// called when the thumbnail size is empty.
TEST_F(NativeDesktopMediaListTest, EmptyThumbnail) {
window_capturer_ = new FakeWindowCapturer();
model_ = std::make_unique<NativeDesktopMediaList>(
DesktopMediaList::Type::kWindow,
base::WrapUnique(window_capturer_.get()));
model_->SetThumbnailSize(gfx::Size());
// Set update period to reduce the time it takes to run tests.
model_->SetUpdatePeriod(base::Milliseconds(20));
base::RunLoop run_loop;
EXPECT_CALL(observer_, OnSourceAdded(0))
.WillOnce(
DoAll(CheckListSize(model_.get(), 1),
QuitRunLoop(base::ThreadTaskRunnerHandle::Get(), &run_loop)));
// Called upon webrtc::DesktopCapturer::CaptureFrame() call.
ON_CALL(observer_, OnSourceThumbnailChanged(_))
.WillByDefault(testing::InvokeWithoutArgs([]() { NOTREACHED(); }));
model_->StartUpdating(&observer_);
AddNativeWindow(WindowIndex(0));
window_capturer_->SetWindowList(window_list_);
run_loop.Run();
EXPECT_EQ(model_->GetSource(0).id.type, DesktopMediaID::TYPE_WINDOW);
EXPECT_EQ(model_->GetSource(0).id.id, WindowIndex(0));
EXPECT_EQ(model_->GetSource(0).thumbnail.size(), gfx::Size());
}