blob: 77f357420ffc0212b0e78f602f6b1656dc591f5e [file] [log] [blame]
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/media/cast_mirroring_service_host.h"
#include "base/containers/contains.h"
#include "base/containers/flat_map.h"
#include "base/json/json_reader.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/bind_post_task.h"
#include "base/test/test_timeouts.h"
#include "build/build_config.h"
#include "chrome/browser/media/router/discovery/access_code/access_code_cast_feature.h"
#include "chrome/browser/media/router/media_router_feature.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/tabs/alert/tab_alert.h"
#include "chrome/browser/ui/tabs/alert/tab_alert_controller.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/tabs/tab_strip_model_delegate.h"
#include "chrome/browser/ui/tabs/tab_strip_model_observer.h"
#include "chrome/browser/ui/tabs/tab_utils.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "components/media_router/common/pref_names.h"
#include "components/mirroring/mojom/cast_message_channel.mojom.h"
#include "components/mirroring/mojom/session_observer.mojom.h"
#include "components/mirroring/mojom/session_parameters.mojom.h"
#include "components/prefs/pref_service.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_features.h"
#include "content/public/test/browser_test.h"
#include "media/base/media_switches.h"
#include "media/capture/mojom/video_capture.mojom.h"
#include "media/capture/mojom/video_capture_buffer.mojom.h"
#include "media/capture/mojom/video_capture_types.mojom.h"
#include "media/capture/video_capture_types.h"
#include "media/mojo/mojom/audio_data_pipe.mojom.h"
#include "media/mojo/mojom/audio_input_stream.mojom.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 "mojo/public/cpp/bindings/remote.h"
#include "testing/gmock/include/gmock/gmock.h"
using testing::_;
using testing::InvokeWithoutArgs;
using testing::Return;
namespace mirroring {
namespace {
media::VideoCaptureParams DefaultVideoCaptureParams() {
constexpr gfx::Size kMaxCaptureSize = gfx::Size(320, 320);
constexpr int kMaxFramesPerSecond = 60;
gfx::Size capture_size = kMaxCaptureSize;
media::VideoCaptureParams params;
params.requested_format = media::VideoCaptureFormat(
capture_size, kMaxFramesPerSecond, media::PIXEL_FORMAT_I420);
params.resolution_change_policy =
media::ResolutionChangePolicy::FIXED_ASPECT_RATIO;
return params;
}
content::DesktopMediaID BuildMediaIdForTabMirroring(
content::WebContents* target_web_contents) {
DCHECK(target_web_contents);
content::DesktopMediaID media_id;
content::RenderFrameHost* const main_frame =
target_web_contents->GetPrimaryMainFrame();
const int process_id = main_frame->GetProcess()->GetDeprecatedID();
const int frame_id = main_frame->GetRoutingID();
media_id.type = content::DesktopMediaID::TYPE_WEB_CONTENTS;
media_id.web_contents_id = content::WebContentsMediaCaptureId(
process_id, frame_id, true /* disable_local_echo */);
return media_id;
}
class MockVideoCaptureObserver final
: public media::mojom::VideoCaptureObserver {
public:
explicit MockVideoCaptureObserver(
mojo::PendingRemote<media::mojom::VideoCaptureHost> host)
: host_(std::move(host)) {}
MockVideoCaptureObserver(const MockVideoCaptureObserver&) = delete;
MockVideoCaptureObserver& operator=(const MockVideoCaptureObserver&) = delete;
MOCK_METHOD1(OnBufferCreatedCall, void(int buffer_id));
MOCK_METHOD1(OnBufferReadyCall, void(int buffer_id));
MOCK_METHOD1(OnBufferDestroyedCall, void(int buffer_id));
MOCK_METHOD1(OnStateChangedCall, void(media::mojom::VideoCaptureState state));
MOCK_METHOD1(OnVideoCaptureErrorCall, void(media::VideoCaptureError error));
MOCK_METHOD1(OnFrameDropped, void(media::VideoCaptureFrameDropReason reason));
// media::mojom::VideoCaptureObserver implementation.
void OnNewBuffer(int32_t buffer_id,
media::mojom::VideoBufferHandlePtr buffer_handle) override {
EXPECT_EQ(buffers_.find(buffer_id), buffers_.end());
EXPECT_EQ(frame_infos_.find(buffer_id), frame_infos_.end());
buffers_[buffer_id] = std::move(buffer_handle);
OnBufferCreatedCall(buffer_id);
}
void OnBufferReady(media::mojom::ReadyBufferPtr buffer) override {
EXPECT_TRUE(buffers_.find(buffer->buffer_id) != buffers_.end());
EXPECT_EQ(frame_infos_.find(buffer->buffer_id), frame_infos_.end());
frame_infos_[buffer->buffer_id] = std::move(buffer->info);
OnBufferReadyCall(buffer->buffer_id);
}
void OnBufferDestroyed(int32_t buffer_id) override {
// The consumer should have finished consuming the buffer before it is being
// destroyed.
EXPECT_TRUE(frame_infos_.find(buffer_id) == frame_infos_.end());
const auto iter = buffers_.find(buffer_id);
EXPECT_TRUE(iter != buffers_.end());
buffers_.erase(iter);
OnBufferDestroyedCall(buffer_id);
}
void OnNewCaptureVersion(
const media::CaptureVersion& capture_version) override {}
void OnStateChanged(media::mojom::VideoCaptureResultPtr result) override {
if (result->which() == media::mojom::VideoCaptureResult::Tag::kState)
OnStateChangedCall(result->get_state());
else
OnVideoCaptureErrorCall(result->get_error_code());
}
void Start() {
host_->Start(device_id_, session_id_, DefaultVideoCaptureParams(),
receiver_.BindNewPipeAndPassRemote());
}
void Stop() { host_->Stop(device_id_); }
void RequestRefreshFrame() { host_->RequestRefreshFrame(device_id_); }
private:
mojo::Remote<media::mojom::VideoCaptureHost> host_;
mojo::Receiver<media::mojom::VideoCaptureObserver> receiver_{this};
base::flat_map<int, media::mojom::VideoBufferHandlePtr> buffers_;
base::flat_map<int, media::mojom::VideoFrameInfoPtr> frame_infos_;
const base::UnguessableToken device_id_ = base::UnguessableToken::Create();
const base::UnguessableToken session_id_ = base::UnguessableToken::Create();
};
class MockCastMessageChannel : public mojom::CastMessageChannel {
public:
// mojom::CastMessageChannel mock implementation (outbound messages).
MOCK_METHOD(void, OnMessage, (mojom::CastMessagePtr));
mojo::Receiver<mojom::CastMessageChannel>* GetChannelReceiver() {
return &channel_receiver_;
}
private:
mojo::Receiver<mojom::CastMessageChannel> channel_receiver_{this};
};
} // namespace
class CastMirroringServiceHostBrowserTest
: public InProcessBrowserTest,
public mojom::SessionObserver,
public mojom::CastMessageChannel,
public mojom::AudioStreamCreatorClient {
public:
CastMirroringServiceHostBrowserTest() = default;
CastMirroringServiceHostBrowserTest(
const CastMirroringServiceHostBrowserTest&) = delete;
CastMirroringServiceHostBrowserTest& operator=(
const CastMirroringServiceHostBrowserTest&) = delete;
~CastMirroringServiceHostBrowserTest() override = default;
protected:
// Starts a tab mirroring session.
void StartTabMirroring() {
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
ASSERT_TRUE(web_contents);
host_ = std::make_unique<CastMirroringServiceHost>(
BuildMediaIdForTabMirroring(web_contents));
mojo::PendingRemote<mojom::SessionObserver> observer;
observer_receiver_.Bind(observer.InitWithNewPipeAndPassReceiver());
mojo::PendingRemote<mojom::CastMessageChannel> outbound_channel;
outbound_channel_receiver_ = std::make_unique<MockCastMessageChannel>();
outbound_channel_receiver_->GetChannelReceiver()->Bind(
outbound_channel.InitWithNewPipeAndPassReceiver());
auto session_params = mojom::SessionParameters::New();
session_params->source_id = "SourceID";
host_->Start(std::move(session_params), std::move(observer),
std::move(outbound_channel),
inbound_channel_.BindNewPipeAndPassReceiver(), "Sink Name");
}
// Starts a tab mirroring session, and sets a target playout delay.
// `media_source_delay` simulates the mirroring delay that is set from a media
// source when starting a mirroring session, i.e. set by a site-initiated
// mirroring source. `feature_delay` is the value that media_router_feature's
// `GetCastMirroringPlayoutDelay` is expected to return.
void StartTabMirroringWithTargetPlayoutDelay(
base::TimeDelta media_source_delay) {
int expected_delay_ms = media_source_delay.InMilliseconds();
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
ASSERT_TRUE(web_contents);
host_ = std::make_unique<CastMirroringServiceHost>(
BuildMediaIdForTabMirroring(web_contents));
mojo::PendingRemote<mojom::SessionObserver> observer;
observer_receiver_.Bind(observer.InitWithNewPipeAndPassReceiver());
mojo::PendingRemote<mojom::CastMessageChannel> outbound_channel;
outbound_channel_receiver_ = std::make_unique<MockCastMessageChannel>();
outbound_channel_receiver_->GetChannelReceiver()->Bind(
outbound_channel.InitWithNewPipeAndPassReceiver());
auto session_params = mojom::SessionParameters::New();
session_params->source_id = "SourceID";
session_params->target_playout_delay = media_source_delay;
base::RunLoop run_loop;
EXPECT_CALL(*outbound_channel_receiver_, OnMessage(_))
.WillOnce([expected_delay_ms,
&run_loop](mirroring::mojom::CastMessagePtr message) {
const std::optional<base::Value> root_or_error =
base::JSONReader::Read(message->json_format_data,
base::JSON_PARSE_CHROMIUM_EXTENSIONS);
ASSERT_TRUE(root_or_error);
const base::Value::Dict& root = root_or_error->GetDict();
const std::string* type = root.FindString("type");
ASSERT_TRUE(type);
if (*type == "OFFER") {
const base::Value::Dict* offer = root.FindDict("offer");
EXPECT_TRUE(offer);
const base::Value::List* streams =
offer->FindList("supportedStreams");
for (auto& stream : *streams) {
const base::Value::Dict& stream_dict = stream.GetDict();
const int stream_target_delay =
stream_dict.FindInt("targetDelay").value();
EXPECT_EQ(stream_target_delay, expected_delay_ms);
}
}
run_loop.Quit();
});
host_->Start(std::move(session_params), std::move(observer),
std::move(outbound_channel),
inbound_channel_.BindNewPipeAndPassReceiver(), "Sink Name");
run_loop.Run();
}
void EnableAccessCodeCast() {
ASSERT_FALSE(media_router::GetAccessCodeCastEnabledPref(
browser()->tab_strip_model()->profile()));
browser()->tab_strip_model()->profile()->GetPrefs()->SetBoolean(
media_router::prefs::kAccessCodeCastEnabled, true);
ASSERT_TRUE(media_router::GetAccessCodeCastEnabledPref(
browser()->tab_strip_model()->profile()));
}
void SwitchTabSource() {
ASSERT_TRUE(host_->tab_switching_ui_enabled_);
browser()->tab_strip_model()->delegate()->AddTabAt(GURL(), -1, true);
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
ASSERT_TRUE(web_contents);
content::FrameTreeNodeId web_contents_source_tab_id =
web_contents->GetPrimaryMainFrame()->GetFrameTreeNodeId();
ASSERT_NE(host_->GetTabSourceId(), web_contents_source_tab_id);
ASSERT_NE(host_->web_contents(), web_contents);
host_->SwitchMirroringSourceTab(BuildMediaIdForTabMirroring(web_contents),
/*captured_surface_control_active=*/false);
ASSERT_EQ(host_->web_contents(), web_contents);
ASSERT_EQ(host_->GetTabSourceId(), web_contents_source_tab_id);
}
void GetVideoCaptureHost() {
mojo::PendingRemote<media::mojom::VideoCaptureHost> video_capture_host;
static_cast<mojom::ResourceProvider*>(host_.get())
->GetVideoCaptureHost(
video_capture_host.InitWithNewPipeAndPassReceiver());
video_frame_receiver_ = std::make_unique<MockVideoCaptureObserver>(
std::move(video_capture_host));
}
void StartVideoCapturing() {
base::RunLoop run_loop;
EXPECT_CALL(*video_frame_receiver_,
OnStateChangedCall(media::mojom::VideoCaptureState::STARTED))
.WillOnce(InvokeWithoutArgs(&run_loop, &base::RunLoop::Quit));
video_frame_receiver_->Start();
run_loop.Run();
}
void PauseMirroring() {
base::RunLoop run_loop;
host_->Pause(run_loop.QuitClosure());
run_loop.Run();
}
void ResumeMirroring() {
base::RunLoop run_loop;
host_->Resume(run_loop.QuitClosure());
run_loop.Run();
}
void StopMirroring() {
if (video_frame_receiver_) {
base::RunLoop run_loop;
EXPECT_CALL(*video_frame_receiver_,
OnStateChangedCall(media::mojom::VideoCaptureState::ENDED))
.WillOnce(InvokeWithoutArgs(&run_loop, &base::RunLoop::Quit));
video_frame_receiver_->Stop();
run_loop.Run();
}
host_.reset();
}
void RequestRefreshFrame() {
base::RunLoop run_loop;
EXPECT_CALL(*video_frame_receiver_, OnBufferReadyCall(_))
.WillOnce(InvokeWithoutArgs(&run_loop, &base::RunLoop::Quit))
.WillRepeatedly(Return());
video_frame_receiver_->RequestRefreshFrame();
run_loop.Run();
}
void CreateAudioLoopbackStream() {
constexpr int kTotalSegments = 1;
constexpr int kAudioTimebase = 48000;
media::AudioParameters params(media::AudioParameters::AUDIO_PCM_LOW_LATENCY,
media::ChannelLayoutConfig::Stereo(),
kAudioTimebase, kAudioTimebase / 100);
base::RunLoop run_loop;
EXPECT_CALL(*this, OnAudioStreamCreated())
.WillOnce(InvokeWithoutArgs(&run_loop, &base::RunLoop::Quit));
host_->CreateAudioStream(audio_client_receiver_.BindNewPipeAndPassRemote(),
params, kTotalSegments);
run_loop.Run();
}
// InProcessBrowserTest override.
void SetUp() override { InProcessBrowserTest::SetUp(); }
std::vector<tabs::TabAlert> GetTabAlertStatesForContents(
content::WebContents* web_contents) {
return tabs::TabAlertController::From(
tabs::TabInterface::GetFromContents(web_contents))
->GetAllActiveAlerts();
}
private:
// mojom::SessionObserver mocks.
MOCK_METHOD(void, OnError, (mojom::SessionError));
MOCK_METHOD(void, DidStart, ());
MOCK_METHOD(void, DidStop, ());
MOCK_METHOD(void, LogInfoMessage, (const std::string&));
MOCK_METHOD(void, LogErrorMessage, (const std::string&));
MOCK_METHOD(void, OnSourceChanged, ());
MOCK_METHOD(void, OnRemotingStateChanged, (bool is_remoting));
// mojom::CastMessageChannel mock implementation (inbound messages).
MOCK_METHOD(void, OnMessage, (mojom::CastMessagePtr));
// mojom::AudioStreamCreatorClient mocks.
MOCK_METHOD(void, OnAudioStreamCreated, ());
void StreamCreated(
mojo::PendingRemote<media::mojom::AudioInputStream> stream,
mojo::PendingReceiver<media::mojom::AudioInputStreamClient>
client_receiver,
media::mojom::ReadWriteAudioDataPipePtr data_pipe) override {
EXPECT_TRUE(stream);
EXPECT_TRUE(client_receiver);
EXPECT_TRUE(data_pipe);
OnAudioStreamCreated();
}
mojo::Receiver<mojom::SessionObserver> observer_receiver_{this};
mojo::Receiver<mojom::AudioStreamCreatorClient> audio_client_receiver_{this};
mojo::Remote<mojom::CastMessageChannel> inbound_channel_;
std::unique_ptr<CastMirroringServiceHost> host_;
std::unique_ptr<MockVideoCaptureObserver> video_frame_receiver_;
std::unique_ptr<MockCastMessageChannel> outbound_channel_receiver_;
};
IN_PROC_BROWSER_TEST_F(CastMirroringServiceHostBrowserTest, CaptureTabVideo) {
StartTabMirroring();
GetVideoCaptureHost();
StartVideoCapturing();
RequestRefreshFrame();
StopMirroring();
}
IN_PROC_BROWSER_TEST_F(CastMirroringServiceHostBrowserTest, CaptureTabAudio) {
StartTabMirroring();
CreateAudioLoopbackStream();
StopMirroring();
}
IN_PROC_BROWSER_TEST_F(CastMirroringServiceHostBrowserTest, TabIndicator) {
content::WebContents* const contents =
browser()->tab_strip_model()->GetActiveWebContents();
ASSERT_THAT(GetTabAlertStatesForContents(contents), ::testing::IsEmpty());
// A TabStripModelObserver that quits the MessageLoop whenever the
// UI's model is sent an event that might change the indicator status.
class IndicatorChangeObserver : public TabStripModelObserver {
public:
explicit IndicatorChangeObserver(Browser* browser) : browser_(browser) {
browser_->tab_strip_model()->AddObserver(this);
}
void TabChangedAt(content::WebContents* contents,
int index,
TabChangeType change_type) override {
std::move(on_tab_changed_).Run();
}
void WaitForTabChange() {
base::RunLoop run_loop;
on_tab_changed_ = run_loop.QuitClosure();
run_loop.Run();
}
private:
const raw_ptr<Browser> browser_;
base::OnceClosure on_tab_changed_;
};
IndicatorChangeObserver observer(browser());
ASSERT_THAT(GetTabAlertStatesForContents(contents), ::testing::IsEmpty());
StartTabMirroring();
// Run the browser until the indicator turns on.
const base::TimeTicks start_time = base::TimeTicks::Now();
while (!base::Contains(GetTabAlertStatesForContents(contents),
tabs::TabAlert::TAB_CAPTURING)) {
if (base::TimeTicks::Now() - start_time >
TestTimeouts::action_max_timeout()) {
EXPECT_THAT(GetTabAlertStatesForContents(contents),
::testing::Contains(tabs::TabAlert::TAB_CAPTURING));
return;
}
observer.WaitForTabChange();
}
StopMirroring();
}
IN_PROC_BROWSER_TEST_F(CastMirroringServiceHostBrowserTest, PauseSession) {
EnableAccessCodeCast();
StartTabMirroring();
GetVideoCaptureHost();
StartVideoCapturing();
PauseMirroring();
ResumeMirroring();
StopMirroring();
}
IN_PROC_BROWSER_TEST_F(CastMirroringServiceHostBrowserTest,
TabMirrorWithPresetPlayoutDelay) {
StartTabMirroringWithTargetPlayoutDelay(base::Milliseconds(200));
GetVideoCaptureHost();
StartVideoCapturing();
RequestRefreshFrame();
StopMirroring();
}
class CastMirroringServiceHostBrowserTestTabSwitcher
: public CastMirroringServiceHostBrowserTest {
public:
CastMirroringServiceHostBrowserTestTabSwitcher() = default;
CastMirroringServiceHostBrowserTestTabSwitcher(
const CastMirroringServiceHostBrowserTestTabSwitcher&) = delete;
CastMirroringServiceHostBrowserTestTabSwitcher& operator=(
const CastMirroringServiceHostBrowserTestTabSwitcher&) = delete;
~CastMirroringServiceHostBrowserTestTabSwitcher() override = default;
};
IN_PROC_BROWSER_TEST_F(CastMirroringServiceHostBrowserTestTabSwitcher,
SwitchTabSource) {
EnableAccessCodeCast();
StartTabMirroring();
GetVideoCaptureHost();
StartVideoCapturing();
SwitchTabSource();
GetVideoCaptureHost();
StartVideoCapturing();
StopMirroring();
}
} // namespace mirroring