blob: 6ab38a02b38be60daa7c4aef826d3729818c0986 [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 <algorithm>
#include "base/barrier_closure.h"
#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/strings/strcat.h"
#include "base/test/bind.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "chrome/browser/extensions/api/desktop_capture/desktop_capture_api.h"
#include "chrome/browser/extensions/scoped_test_mv2_enabler.h"
#include "chrome/browser/media/webrtc/fake_desktop_media_picker_factory.h"
#include "chrome/browser/media/webrtc/webrtc_browsertest_base.h"
#include "chrome/browser/media/webrtc/webrtc_browsertest_common.h"
#include "chrome/browser/sessions/tab_restore_service_factory.h"
#include "chrome/browser/sessions/tab_restore_service_load_waiter.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_tabstrip.h"
#include "chrome/browser/ui/tab_sharing/tab_sharing_infobar_delegate.h"
#include "chrome/browser/ui/tabs/tab_enums.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/infobars/content/content_infobar_manager.h"
#include "components/infobars/core/infobar.h"
#include "components/infobars/core/infobar_manager.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "media/base/media_switches.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "ui/gl/gl_switches.h"
namespace {
static const char kMainWebrtcTestHtmlPage[] = "/webrtc/webrtc_jsep01_test.html";
content::WebContents* GetWebContents(Browser* browser, int tab) {
return browser->tab_strip_model()->GetWebContentsAt(tab);
}
content::DesktopMediaID GetDesktopMediaIDForScreen() {
return content::DesktopMediaID(content::DesktopMediaID::TYPE_SCREEN,
content::DesktopMediaID::kFakeId);
}
content::DesktopMediaID GetDesktopMediaIDForTab(Browser* browser, int tab) {
content::RenderFrameHost* main_frame =
GetWebContents(browser, tab)->GetPrimaryMainFrame();
return content::DesktopMediaID(
content::DesktopMediaID::TYPE_WEB_CONTENTS,
content::DesktopMediaID::kNullId,
content::WebContentsMediaCaptureId(
main_frame->GetProcess()->GetDeprecatedID(),
main_frame->GetRoutingID()));
}
infobars::ContentInfoBarManager* GetInfoBarManager(Browser* browser, int tab) {
return infobars::ContentInfoBarManager::FromWebContents(
GetWebContents(browser, tab));
}
infobars::ContentInfoBarManager* GetInfoBarManager(
content::WebContents* contents) {
return infobars::ContentInfoBarManager::FromWebContents(contents);
}
TabSharingInfoBarDelegate* GetDelegate(Browser* browser, int tab) {
return static_cast<TabSharingInfoBarDelegate*>(
GetInfoBarManager(browser, tab)->infobars()[0]->delegate());
}
class InfobarUIChangeObserver : public TabStripModelObserver {
public:
explicit InfobarUIChangeObserver(Browser* browser) : browser_{browser} {
for (int tab = 0; tab < browser_->tab_strip_model()->count(); ++tab) {
auto* contents = browser_->tab_strip_model()->GetWebContentsAt(tab);
observers_[contents] =
std::make_unique<InfoBarChangeObserver>(base::BindOnce(
&InfobarUIChangeObserver::EraseObserver, base::Unretained(this)));
GetInfoBarManager(contents)->AddObserver(observers_[contents].get());
}
browser_->tab_strip_model()->AddObserver(this);
}
~InfobarUIChangeObserver() override {
for (auto& observer_iter : observers_) {
auto* contents = observer_iter.first;
auto* observer = observer_iter.second.get();
GetInfoBarManager(contents)->RemoveObserver(observer);
}
browser_->tab_strip_model()->RemoveObserver(this);
observers_.clear();
}
void ExpectCalls(size_t expected_changes) {
run_loop_ = std::make_unique<base::RunLoop>();
barrier_closure_ =
base::BarrierClosure(expected_changes, run_loop_->QuitClosure());
for (auto& observer : observers_) {
observer.second->SetCallback(barrier_closure_);
}
}
void Wait() { run_loop_->Run(); }
// TabStripModelObserver
void OnTabStripModelChanged(
TabStripModel* tab_strip_model,
const TabStripModelChange& change,
const TabStripSelectionChange& selection) override {
if (change.type() == TabStripModelChange::kInserted) {
for (const auto& contents_with_index : change.GetInsert()->contents) {
auto* contents = contents_with_index.contents.get();
if (observers_.find(contents) == observers_.end()) {
observers_[contents] = std::make_unique<InfoBarChangeObserver>(
base::BindOnce(&InfobarUIChangeObserver::EraseObserver,
base::Unretained(this)));
GetInfoBarManager(contents)->AddObserver(observers_[contents].get());
if (!barrier_closure_.is_null()) {
observers_[contents]->SetCallback(barrier_closure_);
}
}
}
}
}
void TabChangedAt(content::WebContents* contents,
int index,
TabChangeType change_type) override {
if (observers_.find(contents) == observers_.end()) {
observers_[contents] =
std::make_unique<InfoBarChangeObserver>(base::BindOnce(
&InfobarUIChangeObserver::EraseObserver, base::Unretained(this)));
GetInfoBarManager(contents)->AddObserver(observers_[contents].get());
if (!barrier_closure_.is_null()) {
observers_[contents]->SetCallback(barrier_closure_);
}
}
}
private:
class InfoBarChangeObserver;
public:
void EraseObserver(InfoBarChangeObserver* observer) {
auto iter = std::ranges::find(
observers_, observer,
[](const auto& observer_iter) { return observer_iter.second.get(); });
observers_.erase(iter);
}
private:
class InfoBarChangeObserver : public infobars::InfoBarManager::Observer {
public:
using ShutdownCallback = base::OnceCallback<void(InfoBarChangeObserver*)>;
explicit InfoBarChangeObserver(ShutdownCallback shutdown_callback)
: shutdown_callback_{std::move(shutdown_callback)} {}
~InfoBarChangeObserver() override = default;
void SetCallback(base::RepeatingClosure change_closure) {
DCHECK(!change_closure.is_null());
change_closure_ = change_closure;
}
void OnInfoBarAdded(infobars::InfoBar* infobar) override {
DCHECK(!change_closure_.is_null());
change_closure_.Run();
}
void OnInfoBarRemoved(infobars::InfoBar* infobar, bool animate) override {
DCHECK(!change_closure_.is_null());
change_closure_.Run();
}
void OnInfoBarReplaced(infobars::InfoBar* old_infobar,
infobars::InfoBar* new_infobar) override {
NOTREACHED();
}
void OnManagerShuttingDown(infobars::InfoBarManager* manager) override {
manager->RemoveObserver(this);
DCHECK(!shutdown_callback_.is_null());
std::move(shutdown_callback_).Run(this);
}
private:
base::RepeatingClosure change_closure_;
ShutdownCallback shutdown_callback_;
};
std::unique_ptr<base::RunLoop> run_loop_;
std::map<content::WebContents*, std::unique_ptr<InfoBarChangeObserver>>
observers_;
raw_ptr<Browser> browser_;
base::RepeatingClosure barrier_closure_;
};
} // namespace
// Top-level integration test for WebRTC. Uses an actual desktop capture
// extension to capture the whole screen or a tab.
class WebRtcDesktopCaptureBrowserTest : public WebRtcTestBase {
public:
using MediaIDCallback = base::OnceCallback<content::DesktopMediaID()>;
WebRtcDesktopCaptureBrowserTest() {
extensions::DesktopCaptureChooseDesktopMediaFunction::
SetPickerFactoryForTests(&picker_factory_);
}
void SetUpInProcessBrowserTestFixture() override {
DetectErrorsInJavaScript(); // Look for errors in our rather complex js.
}
void SetUpCommandLine(base::CommandLine* command_line) override {
// Ensure the infobar is enabled, since we expect that in this test.
EXPECT_FALSE(command_line->HasSwitch(switches::kUseFakeUIForMediaStream));
// Flags use to automatically select the right dekstop source and get
// around security restrictions.
command_line->AppendSwitchASCII(switches::kAutoSelectDesktopCaptureSource,
"Entire screen");
command_line->AppendSwitch(switches::kEnableUserMediaScreenCapturing);
// MSan and GL do not get along so avoid using the GPU with MSan.
// TODO(crbug.com/40260482): Remove this after fixing feature
// detection in 0c tab capture path as it'll no longer be needed.
#if !BUILDFLAG(IS_CHROMEOS) && !defined(MEMORY_SANITIZER)
command_line->AppendSwitch(switches::kUseGpuInTests);
#endif
}
protected:
void InitializeTabSharingForFirstTab(
MediaIDCallback media_id_callback,
InfobarUIChangeObserver* observer,
std::optional<std::string> extra_video_constraints = std::nullopt) {
ASSERT_TRUE(embedded_test_server()->Start());
LoadDesktopCaptureExtension();
auto* first_tab = OpenTestPageInNewTab(kMainWebrtcTestHtmlPage);
OpenTestPageInNewTab(kMainWebrtcTestHtmlPage);
FakeDesktopMediaPickerFactory::TestFlags test_flags{
.expect_screens = true,
.expect_windows = true,
.expect_tabs = true,
.picker_result = std::move(media_id_callback).Run(),
};
picker_factory_.SetTestFlags(&test_flags, /*tests_count=*/1);
std::string stream_id = GetDesktopMediaStream(first_tab);
EXPECT_NE(stream_id, "");
LOG(INFO) << "Opened desktop media stream, got id " << stream_id;
std::string constraints = base::StrCat(
{"{audio: false, video: { mandatory: {chromeMediaSource: 'desktop', "
"chromeMediaSourceId: '",
stream_id, "'", (extra_video_constraints.has_value() ? ", " : ""),
extra_video_constraints.value_or(""), "}}}"});
// Should create 3 infobars if a tab (webcontents) is shared!
if (observer)
observer->ExpectCalls(3);
EXPECT_TRUE(GetUserMediaWithSpecificConstraintsAndAcceptIfPrompted(
first_tab, constraints));
if (observer)
observer->Wait();
}
void DetectVideoAndHangUp(content::WebContents* first_tab,
content::WebContents* second_tab) {
StartDetectingVideo(first_tab, "remote-view");
StartDetectingVideo(second_tab, "remote-view");
#if !BUILDFLAG(IS_MAC)
// Video is choppy on Mac OS X. http://crbug.com/443542.
WaitForVideoToPlay(first_tab);
WaitForVideoToPlay(second_tab);
#endif
HangUp(first_tab);
HangUp(second_tab);
}
void RunP2PScreenshareWhileSharing(MediaIDCallback media_id_callback) {
InitializeTabSharingForFirstTab(std::move(media_id_callback), nullptr);
auto* first_tab = browser()->tab_strip_model()->GetWebContentsAt(1);
auto* second_tab = browser()->tab_strip_model()->GetWebContentsAt(2);
GetUserMediaAndAccept(second_tab);
SetupPeerconnectionWithLocalStream(first_tab);
SetupPeerconnectionWithLocalStream(second_tab);
NegotiateCall(first_tab, second_tab);
DetectVideoAndHangUp(first_tab, second_tab);
}
FakeDesktopMediaPickerFactory picker_factory_;
// TODO(https://crbug.com/40804030): Remove this when updated to use MV3.
extensions::ScopedTestMV2Enabler mv2_enabler_;
};
// TODO(crbug.com/40915051): Fails on MAC.
// TODO(crbug.com/40915051): Fails with MSAN. Determine if enabling the test for
// MSAN is feasible or not.
#if BUILDFLAG(IS_MAC)
#define MAYBE_TabCaptureProvidesMinFps DISABLED_TabCaptureProvidesMinFps
#elif defined(MEMORY_SANITIZER)
#define MAYBE_TabCaptureProvidesMinFps DISABLED_TabCaptureProvidesMinFps
#else
#define MAYBE_TabCaptureProvidesMinFps TabCaptureProvidesMinFps
#endif
IN_PROC_BROWSER_TEST_F(WebRtcDesktopCaptureBrowserTest,
MAYBE_TabCaptureProvidesMinFps) {
constexpr int kFps = 30;
constexpr const char* kFpsString = "30";
constexpr int kTestTimeSeconds = 2;
// We wait with measuring frame rate until a few frames has passed. This is
// because the frame rate frame dropper in VideoTrackAdapter is pretty
// aggressive dropping frames when the stream starts.
constexpr int kNumFramesBeforeStabilization = kFps;
InitializeTabSharingForFirstTab(
base::BindOnce(GetDesktopMediaIDForTab, base::Unretained(browser()), 1),
nullptr, base::StrCat({"minFrameRate: ", kFpsString}));
content::WebContents* first_tab =
browser()->tab_strip_model()->GetWebContentsAt(1);
EnableVideoFrameCallbacks(first_tab, "local-view");
// First wait for a frame to appear, then wait until we get the number of
// frames expected during the test time.
int initial_frame_counter = 0;
base::TimeTicks initial_timestamp;
ASSERT_TRUE(test::PollingWaitUntilClosureEvaluatesTrue(
base::BindLambdaForTesting([&]() -> bool {
initial_timestamp = base::TimeTicks::Now();
initial_frame_counter = GetNumVideoFrameCallbacks(first_tab);
return initial_frame_counter > kNumFramesBeforeStabilization;
}),
first_tab, base::Milliseconds(50)));
int final_frame_counter = 0;
base::TimeTicks final_timestamp;
ASSERT_TRUE(test::PollingWaitUntilClosureEvaluatesTrue(
base::BindLambdaForTesting([&]() -> bool {
final_timestamp = base::TimeTicks::Now();
final_frame_counter = GetNumVideoFrameCallbacks(first_tab);
return final_frame_counter >=
kTestTimeSeconds * kFps + initial_frame_counter;
}),
first_tab, base::Milliseconds(50)));
int average_fps = (final_frame_counter - initial_frame_counter) * 1000 /
(final_timestamp - initial_timestamp).InMilliseconds();
// MediaStreamVideoTrack upholds the min fps by way of an idle timer getting
// reset for every received frame from the source. Sources being slow to
// provide frames or plumbed main thread will ensure that the FPS provided is
// actually always strictly lower than the requested minimum.
// Expect at least 1/3 of the expected frames have appeared to aggressively
// combat flakes.
ASSERT_GE(average_fps, kFps / 3);
}
// TODO(crbug.com/40915051): Fails on Linux ASan, LSan and MSan builders.
#if BUILDFLAG(IS_LINUX) && \
((defined(ADDRESS_SANITIZER) && defined(LEAK_SANITIZER)) || \
defined(MEMORY_SANITIZER))
#define MAYBE_TabCaptureProvides0HzWith0MinFpsConstraintAndStaticContent \
DISABLED_TabCaptureProvides0HzWith0MinFpsConstraintAndStaticContent
#else
#define MAYBE_TabCaptureProvides0HzWith0MinFpsConstraintAndStaticContent \
TabCaptureProvides0HzWith0MinFpsConstraintAndStaticContent
#endif
IN_PROC_BROWSER_TEST_F(
WebRtcDesktopCaptureBrowserTest,
MAYBE_TabCaptureProvides0HzWith0MinFpsConstraintAndStaticContent) {
constexpr base::TimeDelta kTestTime = base::Seconds(2);
InitializeTabSharingForFirstTab(
base::BindOnce(GetDesktopMediaIDForTab, base::Unretained(browser()), 1),
nullptr, base::StrCat({"minFrameRate: 0, maxFrameRate: 30"}));
content::WebContents* first_tab =
browser()->tab_strip_model()->GetWebContentsAt(1);
EnableVideoFrameCallbacks(first_tab, "local-view");
// Sample received frame counts during the test time.
int frame_counter = 0;
base::TimeTicks initial_timestamp = base::TimeTicks::Now();
ASSERT_TRUE(test::PollingWaitUntilClosureEvaluatesTrue(
base::BindLambdaForTesting([&]() -> bool {
frame_counter = GetNumVideoFrameCallbacks(first_tab);
return base::TimeTicks::Now() - initial_timestamp >= kTestTime;
}),
first_tab, base::Milliseconds(50)));
// Expect only a few initial frames.
ASSERT_LE(frame_counter, 3);
}
// Flaky on ASan bots. See https://crbug.com/40270173.
// Crashes on some Macs. See https://crbug.com/351095634.
#if defined(ADDRESS_SANITIZER) || defined(MEMORY_SANITIZER) || BUILDFLAG(IS_MAC)
#define MAYBE_RunP2PScreenshareWhileSharingScreen \
DISABLED_RunP2PScreenshareWhileSharingScreen
#else
#define MAYBE_RunP2PScreenshareWhileSharingScreen \
RunP2PScreenshareWhileSharingScreen
#endif
IN_PROC_BROWSER_TEST_F(WebRtcDesktopCaptureBrowserTest,
MAYBE_RunP2PScreenshareWhileSharingScreen) {
RunP2PScreenshareWhileSharing(base::BindOnce(GetDesktopMediaIDForScreen));
}
// Flaky on ASan bots. See https://crbug.com/40270173.
#if defined(ADDRESS_SANITIZER) || defined(MEMORY_SANITIZER)
#define MAYBE_RunP2PScreenshareWhileSharingTab \
DISABLED_RunP2PScreenshareWhileSharingTab
#else
#define MAYBE_RunP2PScreenshareWhileSharingTab RunP2PScreenshareWhileSharingTab
#endif
IN_PROC_BROWSER_TEST_F(WebRtcDesktopCaptureBrowserTest,
MAYBE_RunP2PScreenshareWhileSharingTab) {
RunP2PScreenshareWhileSharing(
base::BindOnce(GetDesktopMediaIDForTab, base::Unretained(browser()), 2));
}
IN_PROC_BROWSER_TEST_F(WebRtcDesktopCaptureBrowserTest,
SwitchSharedTabBackAndForth) {
InfobarUIChangeObserver observer(browser());
InitializeTabSharingForFirstTab(
base::BindOnce(GetDesktopMediaIDForTab, base::Unretained(browser()), 2),
&observer);
// Should delete 3 infobars and create 3 new!
observer.ExpectCalls(6);
// Switch shared tab from 2 to 0.
GetDelegate(browser(), 0)->ShareThisTabInstead();
observer.Wait();
// Should delete 3 infobars and create 3 new!
observer.ExpectCalls(6);
// Switch shared tab from 0 to 2.
GetDelegate(browser(), 2)->ShareThisTabInstead();
observer.Wait();
}
IN_PROC_BROWSER_TEST_F(WebRtcDesktopCaptureBrowserTest,
CloseAndReopenNonSharedTab) {
InfobarUIChangeObserver observer(browser());
InitializeTabSharingForFirstTab(
base::BindOnce(GetDesktopMediaIDForTab, base::Unretained(browser()), 2),
&observer);
// Should delete 1 infobar.
observer.ExpectCalls(1);
// Close non-shared and non-sharing, i.e., unrelated tab
browser()->tab_strip_model()->CloseWebContentsAt(
0, TabCloseTypes::CLOSE_CREATE_HISTORICAL_TAB);
observer.Wait();
// Should create 1 infobar.
observer.ExpectCalls(1);
// Restore tab
chrome::RestoreTab(browser());
observer.Wait();
}