blob: ebfb2c174b44705a24b3071633bcd3cfc65e42f8 [file] [log] [blame]
// Copyright 2015 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/router/mojo/media_router_desktop.h"
#include <stddef.h>
#include <stdint.h>
#include <memory>
#include <string>
#include <string_view>
#include <utility>
#include "base/base64.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/json/string_escape.h"
#include "base/memory/ref_counted.h"
#include "base/notimplemented.h"
#include "base/run_loop.h"
#include "base/strings/strcat.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/mock_callback.h"
#include "chrome/browser/media/router/media_router_feature.h"
#include "chrome/browser/media/router/mojo/media_router_mojo_metrics.h"
#include "chrome/browser/media/router/test/media_router_mojo_test.h"
#include "chrome/browser/media/router/test/provider_test_helpers.h"
#include "chrome/test/base/chrome_render_view_host_test_harness.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile.h"
#include "components/media_router/browser/presentation_connection_message_observer.h"
#include "components/media_router/browser/route_message_util.h"
#include "components/media_router/browser/test/mock_media_router.h"
#include "components/media_router/common/issue.h"
#include "components/media_router/common/media_route.h"
#include "components/media_router/common/media_source.h"
#include "components/media_router/common/test/test_helper.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
using blink::mojom::PresentationConnectionCloseReason;
using blink::mojom::PresentationConnectionState;
using media_router::mojom::RouteMessagePtr;
using testing::_;
using testing::Eq;
using testing::Invoke;
using testing::InvokeWithoutArgs;
using testing::IsEmpty;
using testing::Mock;
using testing::NiceMock;
using testing::Not;
using testing::Return;
using testing::SaveArg;
using testing::Sequence;
using testing::SizeIs;
using testing::StrictMock;
using testing::UnorderedElementsAre;
using testing::WithArg;
namespace media_router {
namespace {
const char kDescription[] = "description";
const char kError[] = "error";
const char kSource[] = "source1";
const char kSource2[] = "source2";
const char kTabSourceOne[] = "urn:x-org.chromium.media:source:tab:1";
const char kTabSourceTwo[] = "urn:x-org.chromium.media:source:tab:2";
const char kRouteId[] = "routeId";
const char kRouteId2[] = "routeId2";
const char kSinkId[] = "sink";
const char kSinkId2[] = "sink2";
const char kSinkName[] = "sinkName";
const char kPresentationId[] = "presentationId";
const char kOrigin[] = "http://origin/";
const int kInvalidFrameNodeId = -1;
const int kTimeoutMillis = 5 * 1000;
IssueInfo CreateIssueInfo(const std::string& title) {
IssueInfo issue_info;
issue_info.title = title;
issue_info.message = std::string("msg");
issue_info.severity = IssueInfo::Severity::WARNING;
return issue_info;
}
// Creates a media route whose ID is |kRouteId|.
MediaRoute CreateMediaRoute() {
MediaRoute route(kRouteId, MediaSource(kSource), kSinkId, kDescription, true);
route.set_presentation_id(kPresentationId);
route.set_controller_type(RouteControllerType::kGeneric);
return route;
}
// Creates a media route whose ID is |kRouteId2|.
MediaRoute CreateMediaRoute2() {
MediaRoute route(kRouteId2, MediaSource(kSource), kSinkId, kDescription,
true);
route.set_presentation_id(kPresentationId);
route.set_controller_type(RouteControllerType::kGeneric);
return route;
}
std::string RouteMessageToString(const RouteMessagePtr& message) {
if (!message->message && !message->data) {
return "null";
}
std::string result;
if (message->message) {
result = "text=";
base::EscapeJSONString(message->message.value(), true, &result);
} else {
const std::string_view src(
reinterpret_cast<const char*>(message->data.value().data()),
message->data.value().size());
result = "binary=" + base::Base64Encode(src);
}
return result;
}
std::vector<MediaSinkInternal> ToInternalSinks(
const std::vector<MediaSink>& sinks) {
std::vector<MediaSinkInternal> internal_sinks;
internal_sinks.reserve(sinks.size());
for (const auto& sink : sinks) {
MediaSinkInternal internal_sink;
internal_sink.set_sink(sink);
internal_sinks.emplace_back(std::move(internal_sink));
}
return internal_sinks;
}
class StubMediaRouterDesktop : public MediaRouterDesktop {
public:
explicit StubMediaRouterDesktop(content::BrowserContext* context)
: MediaRouterDesktop(context) {}
~StubMediaRouterDesktop() override = default;
// media_router::MediaRouter: implementation
base::Value::Dict GetState() const override {
NOTIMPLEMENTED();
return base::Value::Dict();
}
void GetProviderState(
mojom::MediaRouteProviderId provider_id,
mojom::MediaRouteProvider::GetStateCallback callback) const override {
NOTIMPLEMENTED();
}
};
} // namespace
class MediaRouterDesktopTest : public MediaRouterMojoTest {
public:
MediaRouterDesktopTest() = default;
~MediaRouterDesktopTest() override = default;
protected:
void ExpectCastResultBucketCount(const std::string& operation,
mojom::RouteRequestResultCode result_code,
int expected_count) {
ExpectBucketCount("MediaRouter.Provider." + operation + ".Result.Cast",
result_code, expected_count);
}
void ExpectResultBucketCount(const std::string& operation,
mojom::RouteRequestResultCode result_code,
int expected_count) {
ExpectBucketCount("MediaRouter.Provider." + operation + ".Result",
result_code, expected_count);
}
std::unique_ptr<MediaRouterDesktop> CreateMediaRouter() override {
auto router = std::unique_ptr<MediaRouterDesktop>(
new StubMediaRouterDesktop(profile()));
router->DisableMediaRouteProvidersForTest();
return router;
}
void ReceiveSinks(mojom::MediaRouteProviderId provider_id,
const std::string& media_source,
const std::vector<MediaSinkInternal>& sinks) {
router()->OnSinksReceived(
provider_id, media_source, sinks,
std::vector<url::Origin>{1, url::Origin::Create(GURL(kOrigin))});
}
void ReceiveRouteMessages(const std::string& route_id,
std::vector<mojom::RouteMessagePtr> messages) {
router()->OnRouteMessagesReceived(route_id, std::move(messages));
}
void RegisterMediaRoutesObserver(MediaRoutesObserver* observer) {
router()->RegisterMediaRoutesObserver(observer);
}
void UnregisterMediaRoutesObserver(MediaRoutesObserver* observer) {
router()->UnregisterMediaRoutesObserver(observer);
}
void UpdateRoutes(mojom::MediaRouteProviderId provider_id,
const std::vector<MediaRoute>& routes) {
router()->OnRoutesUpdated(provider_id, routes);
}
void RecordPresentationRequestUrlBySink(
const media_router::MediaSource& source,
media_router::mojom::MediaRouteProviderId provider_id,
int times) {
for (int i = 0; i < times; i++) {
MediaRouterDesktop::RecordPresentationRequestUrlBySink(source,
provider_id);
}
}
void OnUserGesture() { router()->OnUserGesture(); }
void SetMockSinkServiceForTest(MockDualMediaSinkService* sink_service) {
router()->media_sink_service_ = sink_service;
}
private:
void ExpectBucketCount(const std::string& histogram_name,
mojom::RouteRequestResultCode result_code,
int expected_count) {
histogram_tester_.ExpectBucketCount(histogram_name, result_code,
expected_count);
}
base::HistogramTester histogram_tester_;
};
TEST_F(MediaRouterDesktopTest, CreateRoute) {
TestCreateRoute();
ExpectCastResultBucketCount("CreateRoute", mojom::RouteRequestResultCode::OK,
1);
}
// Tests that MediaRouter is aware when a route is created, even if
// MediaRouteProvider doesn't call OnRoutesUpdated().
TEST_F(MediaRouterDesktopTest, RouteRecognizedAfterCreation) {
MockMediaRoutesObserver routes_observer1(router());
MockMediaRoutesObserver routes_observer2(router());
EXPECT_CALL(routes_observer2, OnRoutesUpdated(SizeIs(0)));
EXPECT_CALL(routes_observer2, OnRoutesUpdated(SizeIs(1)));
// TestCreateRoute() does not explicitly call OnRoutesUpdated() on the router.
TestCreateRoute();
base::RunLoop().RunUntilIdle();
}
TEST_F(MediaRouterDesktopTest, CreateRouteFails) {
ProvideTestSink(mojom::MediaRouteProviderId::CAST, kSinkId);
EXPECT_CALL(mock_cast_provider_,
CreateRouteInternal(kSource, kSinkId, _,
url::Origin::Create(GURL(kOrigin)),
kInvalidFrameNodeId, _, _))
.WillOnce(
WithArg<6>([](mojom::MediaRouteProvider::CreateRouteCallback& cb) {
std::move(cb).Run(std::nullopt, nullptr, std::string(kError),
mojom::RouteRequestResultCode::TIMED_OUT);
}));
RouteResponseCallbackHandler handler;
base::RunLoop run_loop;
EXPECT_CALL(handler, DoInvoke(nullptr, "", kError,
mojom::RouteRequestResultCode::TIMED_OUT, _))
.WillOnce(InvokeWithoutArgs([&run_loop]() { run_loop.Quit(); }));
router()->CreateRoute(kSource, kSinkId, url::Origin::Create(GURL(kOrigin)),
nullptr,
base::BindOnce(&RouteResponseCallbackHandler::Invoke,
base::Unretained(&handler)),
base::Milliseconds(kTimeoutMillis));
run_loop.Run();
ExpectCastResultBucketCount("CreateRoute",
mojom::RouteRequestResultCode::TIMED_OUT, 1);
}
TEST_F(MediaRouterDesktopTest, JoinRoute) {
TestJoinRoute(kPresentationId);
ExpectCastResultBucketCount("JoinRoute", mojom::RouteRequestResultCode::OK,
1);
}
TEST_F(MediaRouterDesktopTest, JoinRouteNotFoundFails) {
RouteResponseCallbackHandler handler;
base::RunLoop run_loop;
EXPECT_CALL(handler,
DoInvoke(nullptr, "", "Route not found",
mojom::RouteRequestResultCode::ROUTE_NOT_FOUND, _))
.WillOnce(InvokeWithoutArgs([&run_loop]() { run_loop.Quit(); }));
router()->JoinRoute(kSource, kPresentationId,
url::Origin::Create(GURL(kOrigin)), nullptr,
base::BindOnce(&RouteResponseCallbackHandler::Invoke,
base::Unretained(&handler)),
base::Milliseconds(kTimeoutMillis));
run_loop.Run();
ExpectResultBucketCount("JoinRoute",
mojom::RouteRequestResultCode::ROUTE_NOT_FOUND, 1);
}
TEST_F(MediaRouterDesktopTest, JoinRouteTimedOutFails) {
// Make sure the MR has received an update with the route, so it knows there
// is a route to join.
const std::vector<MediaRoute> routes{CreateMediaRoute()};
UpdateRoutes(mojom::MediaRouteProviderId::CAST, routes);
EXPECT_TRUE(router()->HasJoinableRoute());
EXPECT_CALL(mock_cast_provider_,
JoinRouteInternal(
kSource, kPresentationId, url::Origin::Create(GURL(kOrigin)),
kInvalidFrameNodeId, base::Milliseconds(kTimeoutMillis), _))
.WillOnce(
WithArg<5>([](mojom::MediaRouteProvider::JoinRouteCallback& cb) {
std::move(cb).Run(std::nullopt, nullptr, std::string(kError),
mojom::RouteRequestResultCode::TIMED_OUT);
}));
RouteResponseCallbackHandler handler;
base::RunLoop run_loop;
EXPECT_CALL(handler, DoInvoke(nullptr, "", kError,
mojom::RouteRequestResultCode::TIMED_OUT, _))
.WillOnce(InvokeWithoutArgs([&run_loop]() { run_loop.Quit(); }));
router()->JoinRoute(kSource, kPresentationId,
url::Origin::Create(GURL(kOrigin)), nullptr,
base::BindOnce(&RouteResponseCallbackHandler::Invoke,
base::Unretained(&handler)),
base::Milliseconds(kTimeoutMillis));
run_loop.Run();
ExpectCastResultBucketCount("JoinRoute",
mojom::RouteRequestResultCode::TIMED_OUT, 1);
}
TEST_F(MediaRouterDesktopTest, DetachRoute) {
TestDetachRoute();
}
TEST_F(MediaRouterDesktopTest, TerminateRoute) {
TestTerminateRoute();
ExpectCastResultBucketCount("TerminateRoute",
mojom::RouteRequestResultCode::OK, 1);
}
TEST_F(MediaRouterDesktopTest, TerminateRouteFails) {
ProvideTestRoute(mojom::MediaRouteProviderId::CAST, kRouteId);
EXPECT_CALL(mock_cast_provider_, TerminateRouteInternal(kRouteId, _))
.WillOnce([](const std::string& route_id,
mojom::MediaRouteProvider::TerminateRouteCallback& cb) {
std::move(cb).Run(std::string("timed out"),
mojom::RouteRequestResultCode::TIMED_OUT);
});
router()->TerminateRoute(kRouteId);
base::RunLoop().RunUntilIdle();
ExpectCastResultBucketCount("TerminateRoute",
mojom::RouteRequestResultCode::OK, 0);
ExpectCastResultBucketCount("TerminateRoute",
mojom::RouteRequestResultCode::TIMED_OUT, 1);
}
#if !BUILDFLAG(IS_ANDROID)
TEST_F(MediaRouterDesktopTest, HandleIssue) {
MockIssuesObserver issue_observer1(router()->GetIssueManager());
MockIssuesObserver issue_observer2(router()->GetIssueManager());
issue_observer1.Init();
issue_observer2.Init();
IssueInfo issue_info = CreateIssueInfo("title 1");
Issue issue_from_observer1(Issue::CreateIssueWithIssueInfo(IssueInfo()));
Issue issue_from_observer2(Issue::CreateIssueWithIssueInfo(IssueInfo()));
EXPECT_CALL(issue_observer1, OnIssue(_))
.WillOnce(SaveArg<0>(&issue_from_observer1));
EXPECT_CALL(issue_observer2, OnIssue(_))
.WillOnce(SaveArg<0>(&issue_from_observer2));
router()->OnIssue(issue_info);
base::RunLoop().RunUntilIdle();
ASSERT_EQ(issue_from_observer1.id(), issue_from_observer2.id());
EXPECT_EQ(issue_info, issue_from_observer1.info());
EXPECT_EQ(issue_info, issue_from_observer2.info());
}
TEST_F(MediaRouterDesktopTest, HandlePermissionIssue) {
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeature(
media_router::kShowCastPermissionRejectedError);
MockIssuesObserver issue_observer(router()->GetIssueManager());
issue_observer.Init();
base::RunLoop run_loop;
EXPECT_CALL(issue_observer, OnIssue(_))
.WillOnce([&run_loop](const Issue& received_issue) {
EXPECT_TRUE(received_issue.is_permission_rejected_issue());
run_loop.QuitClosure().Run();
});
router()->OnLocalDiscoveryPermissionRejected();
run_loop.Run();
}
#endif // !BUILDFLAG(IS_ANDROID)
TEST_F(MediaRouterDesktopTest, RegisterAndUnregisterMediaSinksObserver) {
MediaSource media_source(kSource);
// These should only be called once even if there is more than one observer
// for a given source.
EXPECT_CALL(mock_cast_provider_, StartObservingMediaSinks(kSource));
EXPECT_CALL(mock_cast_provider_, StartObservingMediaSinks(kSource2));
auto sinks_observer = std::make_unique<NiceMock<MockMediaSinksObserver>>(
router(), media_source, url::Origin::Create(GURL(kOrigin)));
EXPECT_TRUE(sinks_observer->Init());
auto extra_sinks_observer =
std::make_unique<NiceMock<MockMediaSinksObserver>>(
router(), media_source, url::Origin::Create(GURL(kOrigin)));
EXPECT_TRUE(extra_sinks_observer->Init());
auto unrelated_sinks_observer =
std::make_unique<NiceMock<MockMediaSinksObserver>>(
router(), MediaSource(kSource2), url::Origin::Create(GURL(kOrigin)));
EXPECT_TRUE(unrelated_sinks_observer->Init());
base::RunLoop().RunUntilIdle();
const std::vector<MediaSink> kExpectedSinks{
CreateCastSink(kSinkId, kSinkName), CreateCastSink(kSinkId2, kSinkName)};
EXPECT_CALL(*sinks_observer, OnSinksReceived(kExpectedSinks));
EXPECT_CALL(*extra_sinks_observer, OnSinksReceived(kExpectedSinks));
ReceiveSinks(mojom::MediaRouteProviderId::CAST, media_source.id(),
ToInternalSinks(kExpectedSinks));
// Since the MediaRouterDesktop has already received results for
// |media_source|, return cached results to observers that are subsequently
// registered.
auto cached_sinks_observer = std::make_unique<MockMediaSinksObserver>(
router(), media_source, url::Origin::Create(GURL(kOrigin)));
EXPECT_CALL(*cached_sinks_observer, OnSinksReceived(kExpectedSinks));
EXPECT_TRUE(cached_sinks_observer->Init());
// Different origin from cached result. Empty list will be returned.
auto cached_sinks_observer2 = std::make_unique<MockMediaSinksObserver>(
router(), media_source, url::Origin::Create(GURL("https://youtube.com")));
EXPECT_CALL(*cached_sinks_observer2, OnSinksReceived(IsEmpty()));
EXPECT_TRUE(cached_sinks_observer2->Init());
EXPECT_CALL(mock_cast_provider_, StopObservingMediaSinks(kSource));
EXPECT_CALL(mock_cast_provider_, StopObservingMediaSinks(kSource2));
sinks_observer.reset();
extra_sinks_observer.reset();
unrelated_sinks_observer.reset();
cached_sinks_observer.reset();
cached_sinks_observer2.reset();
base::RunLoop().RunUntilIdle();
}
TEST_F(MediaRouterDesktopTest, TabSinksObserverIsShared) {
MediaSource tab_source_one(kTabSourceOne);
MediaSource tab_source_two(kTabSourceTwo);
// Media router should not try to start observing for each tab, but instead
// only once for all tabs.
EXPECT_CALL(mock_cast_provider_, StartObservingMediaSinks(kTabSourceOne))
.Times(0);
EXPECT_CALL(mock_cast_provider_, StartObservingMediaSinks(kTabSourceTwo))
.Times(0);
EXPECT_CALL(mock_cast_provider_,
StartObservingMediaSinks(MediaSource::ForAnyTab().id()))
.Times(1);
const auto origin = url::Origin::Create(GURL(kOrigin));
auto sinks_observer = std::make_unique<NiceMock<MockMediaSinksObserver>>(
router(), tab_source_one, origin);
EXPECT_TRUE(sinks_observer->Init());
auto extra_sinks_observer =
std::make_unique<NiceMock<MockMediaSinksObserver>>(
router(), tab_source_one, origin);
EXPECT_TRUE(extra_sinks_observer->Init());
auto second_tab_sinks_observer =
std::make_unique<NiceMock<MockMediaSinksObserver>>(
router(), MediaSource(kTabSourceTwo), origin);
EXPECT_TRUE(second_tab_sinks_observer->Init());
base::RunLoop().RunUntilIdle();
const std::vector<MediaSink> kExpectedSinks{
CreateCastSink(kSinkId, kSinkName), CreateCastSink(kSinkId2, kSinkName)};
// All tabs should get the same updates.
EXPECT_CALL(*sinks_observer, OnSinksReceived(kExpectedSinks));
EXPECT_CALL(*extra_sinks_observer, OnSinksReceived(kExpectedSinks));
EXPECT_CALL(*second_tab_sinks_observer, OnSinksReceived(kExpectedSinks));
ReceiveSinks(mojom::MediaRouteProviderId::CAST, tab_source_one.id(),
ToInternalSinks(kExpectedSinks));
// Since the MediaRouterDesktop has already received results for
// |media_source|, return cached results to observers that are subsequently
// registered.
auto cached_sinks_observer = std::make_unique<MockMediaSinksObserver>(
router(), tab_source_one, url::Origin::Create(GURL(kOrigin)));
EXPECT_CALL(*cached_sinks_observer, OnSinksReceived(kExpectedSinks));
EXPECT_TRUE(cached_sinks_observer->Init());
// Different origin from cached result. Empty list will be returned.
auto cached_sinks_observer2 = std::make_unique<MockMediaSinksObserver>(
router(), tab_source_one,
url::Origin::Create(GURL("https://youtube.com")));
EXPECT_CALL(*cached_sinks_observer2, OnSinksReceived(IsEmpty()));
EXPECT_TRUE(cached_sinks_observer2->Init());
// Since tabs share observation, stop observing should not be called.
EXPECT_CALL(mock_cast_provider_, StopObservingMediaSinks(kTabSourceOne))
.Times(0);
EXPECT_CALL(mock_cast_provider_, StopObservingMediaSinks(kTabSourceTwo))
.Times(0);
EXPECT_CALL(mock_cast_provider_,
StopObservingMediaSinks(MediaSource::ForAnyTab().id()))
.Times(0);
sinks_observer.reset();
extra_sinks_observer.reset();
second_tab_sinks_observer.reset();
cached_sinks_observer.reset();
cached_sinks_observer2.reset();
base::RunLoop().RunUntilIdle();
}
TEST_F(MediaRouterDesktopTest, RegisterAndUnregisterMediaRoutesObserver) {
MockMediaRouter mock_router;
auto routes_observer =
std::make_unique<MockMediaRoutesObserver>(&mock_router);
EXPECT_TRUE(
mock_router.routes_observers().HasObserver(routes_observer.get()));
auto extra_routes_observer =
std::make_unique<MockMediaRoutesObserver>(&mock_router);
EXPECT_TRUE(
mock_router.routes_observers().HasObserver(extra_routes_observer.get()));
routes_observer.reset();
extra_routes_observer.reset();
EXPECT_TRUE(mock_router.routes_observers().empty());
}
TEST_F(MediaRouterDesktopTest, UnregisterBeforeNotificationDoesntCrash) {
auto routes_observer = std::make_unique<MediaRoutesObserver>(router());
auto routes_observer_two = std::make_unique<MediaRoutesObserver>(router());
// Resetting the observer immediately should cause it to be invalidated before
// the callback for NotifyOfExistingRoutesIfRegistered is called.
routes_observer_two.reset();
// Make sure the Notify task executes.
base::RunLoop().RunUntilIdle();
}
TEST_F(MediaRouterDesktopTest, RegisteredObserversGetMediaRouteUpdates) {
auto routes_observer =
std::make_unique<StrictMock<MockMediaRoutesObserver>>(router());
auto extra_routes_observer =
std::make_unique<StrictMock<MockMediaRoutesObserver>>(router());
MediaSource media_source(kSource);
std::vector<MediaRoute> expected_routes{
MediaRoute(kRouteId, media_source, kSinkId, kDescription, false)};
EXPECT_CALL(*routes_observer, OnRoutesUpdated(expected_routes)).Times(1);
EXPECT_CALL(*extra_routes_observer, OnRoutesUpdated(expected_routes))
.Times(1);
UpdateRoutes(mojom::MediaRouteProviderId::CAST, expected_routes);
routes_observer.reset();
extra_routes_observer.reset();
// No route observers should be notified.
UpdateRoutes(mojom::MediaRouteProviderId::CAST, expected_routes);
}
TEST_F(MediaRouterDesktopTest, SendRouteMessage) {
TestSendRouteMessage();
}
TEST_F(MediaRouterDesktopTest, SendRouteBinaryMessage) {
TestSendRouteBinaryMessage();
}
namespace {
// Used in the RouteMessages* tests to populate the messages that will be
// processed and dispatched to PresentationConnectionMessageObservers.
void PopulateRouteMessages(std::vector<RouteMessagePtr>* batch1,
std::vector<RouteMessagePtr>* batch2,
std::vector<RouteMessagePtr>* batch3,
std::vector<RouteMessagePtr>* all_messages) {
batch1->clear();
batch2->clear();
batch3->clear();
batch1->emplace_back(message_util::RouteMessageFromString("text1"));
batch2->emplace_back(
message_util::RouteMessageFromData(std::vector<uint8_t>({UINT8_C(1)})));
batch2->emplace_back(message_util::RouteMessageFromString("text2"));
batch3->emplace_back(message_util::RouteMessageFromString("text3"));
batch3->emplace_back(
message_util::RouteMessageFromData(std::vector<uint8_t>({UINT8_C(2)})));
batch3->emplace_back(
message_util::RouteMessageFromData(std::vector<uint8_t>({UINT8_C(3)})));
all_messages->clear();
for (auto* message_batch : {batch1, batch2, batch3}) {
for (auto& message : *message_batch) {
all_messages->emplace_back(message->Clone());
}
}
}
// Used in the RouteMessages* tests to observe and sanity-check that the
// messages being received from the router are correct and in-sequence. The
// checks here correspond to the expected messages in PopulateRouteMessages()
// above.
class ExpectedMessagesObserver final
: public PresentationConnectionMessageObserver {
public:
ExpectedMessagesObserver(MediaRouter* router,
const MediaRoute::Id& route_id,
std::vector<RouteMessagePtr> expected_messages)
: PresentationConnectionMessageObserver(router, route_id),
expected_messages_(std::move(expected_messages)) {}
~ExpectedMessagesObserver() override { CheckReceivedMessages(); }
private:
void OnMessagesReceived(std::vector<RouteMessagePtr> messages) override {
for (auto& message : messages) {
messages_.emplace_back(std::move(message));
}
}
void CheckReceivedMessages() {
ASSERT_EQ(expected_messages_.size(), messages_.size());
for (size_t i = 0; i < expected_messages_.size(); i++) {
EXPECT_EQ(expected_messages_[i], messages_[i])
<< "Message mismatch at index " << i
<< ": expected: " << RouteMessageToString(expected_messages_[i])
<< ", actual: " << RouteMessageToString(messages_[i]);
}
}
std::vector<RouteMessagePtr> expected_messages_;
std::vector<RouteMessagePtr> messages_;
};
} // namespace
TEST_F(MediaRouterDesktopTest, RouteMessagesSingleObserver) {
std::vector<RouteMessagePtr> incoming_batch1, incoming_batch2,
incoming_batch3, all_messages;
ProvideTestRoute(mojom::MediaRouteProviderId::CAST, kRouteId);
PopulateRouteMessages(&incoming_batch1, &incoming_batch2, &incoming_batch3,
&all_messages);
// Creating ExpectedMessagesObserver will register itself to the
// MediaRouter, which in turn will start listening for route messages.
ExpectedMessagesObserver observer(router(), kRouteId,
std::move(all_messages));
ReceiveRouteMessages(kRouteId, std::move(incoming_batch1));
ReceiveRouteMessages(kRouteId, std::move(incoming_batch2));
ReceiveRouteMessages(kRouteId, std::move(incoming_batch3));
// When |observer| goes out-of-scope, its destructor will ensure all expected
// messages have been received.
}
TEST_F(MediaRouterDesktopTest, RouteMessagesMultipleObservers) {
std::vector<RouteMessagePtr> incoming_batch1, incoming_batch2,
incoming_batch3, all_messages;
ProvideTestRoute(mojom::MediaRouteProviderId::CAST, kRouteId);
PopulateRouteMessages(&incoming_batch1, &incoming_batch2, &incoming_batch3,
&all_messages);
std::vector<RouteMessagePtr> all_messages2;
for (auto& message : all_messages) {
all_messages2.emplace_back(message->Clone());
}
// The ExpectedMessagesObservers will register themselves with the
// MediaRouter, which in turn will start listening for route messages.
ExpectedMessagesObserver observer1(router(), kRouteId,
std::move(all_messages));
ExpectedMessagesObserver observer2(router(), kRouteId,
std::move(all_messages2));
ReceiveRouteMessages(kRouteId, std::move(incoming_batch1));
ReceiveRouteMessages(kRouteId, std::move(incoming_batch2));
ReceiveRouteMessages(kRouteId, std::move(incoming_batch3));
// As each |observer| goes out-of-scope, its destructor will ensure all
// expected messages have been received.
}
TEST_F(MediaRouterDesktopTest, PresentationConnectionStateChangedCallback) {
MediaRoute::Id route_id("route-id");
const GURL presentation_url("http://www.example.com/presentation.html");
const std::string presentation_id("pid");
blink::mojom::PresentationInfo connection(presentation_url, presentation_id);
base::MockCallback<content::PresentationConnectionStateChangedCallback>
callback;
base::CallbackListSubscription subscription =
router()->AddPresentationConnectionStateChangedCallback(route_id,
callback.Get());
{
base::RunLoop run_loop;
content::PresentationConnectionStateChangeInfo closed_info(
PresentationConnectionState::CLOSED);
closed_info.close_reason = PresentationConnectionCloseReason::WENT_AWAY;
closed_info.message = "Foo";
EXPECT_CALL(callback, Run(StateChangeInfoEquals(closed_info)))
.WillOnce(InvokeWithoutArgs([&run_loop]() { run_loop.Quit(); }));
router()->OnPresentationConnectionClosed(
route_id, PresentationConnectionCloseReason::WENT_AWAY, "Foo");
run_loop.Run();
EXPECT_TRUE(Mock::VerifyAndClearExpectations(&callback));
}
content::PresentationConnectionStateChangeInfo terminated_info(
PresentationConnectionState::TERMINATED);
{
base::RunLoop run_loop;
EXPECT_CALL(callback, Run(StateChangeInfoEquals(terminated_info)))
.WillOnce(InvokeWithoutArgs([&run_loop]() { run_loop.Quit(); }));
router()->OnPresentationConnectionStateChanged(
route_id, PresentationConnectionState::TERMINATED);
run_loop.Run();
EXPECT_TRUE(Mock::VerifyAndClearExpectations(&callback));
}
}
TEST_F(MediaRouterDesktopTest,
PresentationConnectionStateChangedCallbackRemoved) {
MediaRoute::Id route_id("route-id");
base::MockCallback<content::PresentationConnectionStateChangedCallback>
callback;
base::CallbackListSubscription subscription =
router()->AddPresentationConnectionStateChangedCallback(route_id,
callback.Get());
// Callback has been removed, so we don't expect it to be called anymore.
subscription = {};
EXPECT_TRUE(router()->presentation_connection_state_callbacks_.empty());
EXPECT_CALL(callback, Run(_)).Times(0);
router()->OnPresentationConnectionStateChanged(
route_id, PresentationConnectionState::TERMINATED);
base::RunLoop().RunUntilIdle();
}
TEST_F(MediaRouterDesktopTest, GetMediaController) {
MockMediaController mock_controller;
mojo::Remote<mojom::MediaController> controller_remote;
mojo::PendingRemote<mojom::MediaStatusObserver> observer_remote;
MockMediaStatusObserver mock_observer(
observer_remote.InitWithNewPipeAndPassReceiver());
mojo::Remote<mojom::MediaStatusObserver> observer_remote_held_by_controller;
UpdateRoutes(mojom::MediaRouteProviderId::CAST, {CreateMediaRoute()});
EXPECT_CALL(mock_cast_provider_,
BindMediaControllerInternal(kRouteId, _, _, _))
.WillOnce(
[&](const std::string& route_id,
mojo::PendingReceiver<mojom::MediaController>& media_controller,
mojo::PendingRemote<mojom::MediaStatusObserver>& observer,
MockMediaRouteProvider::BindMediaControllerCallback& callback) {
mock_controller.Bind(std::move(media_controller));
observer_remote_held_by_controller.Bind(std::move(observer));
std::move(callback).Run(true);
});
router()->GetMediaController(kRouteId,
controller_remote.BindNewPipeAndPassReceiver(),
std::move(observer_remote));
base::RunLoop().RunUntilIdle();
EXPECT_CALL(mock_controller, Play());
controller_remote->Play();
EXPECT_CALL(mock_observer, OnMediaStatusUpdated(_));
observer_remote_held_by_controller->OnMediaStatusUpdated(
mojom::MediaStatus::New());
base::RunLoop().RunUntilIdle();
}
TEST_F(MediaRouterDesktopTest, SendSinkRequestsToMultipleProviders) {
// Register |mock_wired_display_provider_| with the router.
// |mock_cast_provider_| has already been registered during setup.
RegisterWiredDisplayProvider();
// Set |mock_cast_provider_| to have one sink with ID |kSinkId|.
ProvideTestSink(mojom::MediaRouteProviderId::CAST, kSinkId);
mock_cast_provider_.SetRouteToReturn(CreateMediaRoute());
// Set |mock_wired_display_provider_| to have one sink with ID |kSinkId2|.
ProvideTestSink(mojom::MediaRouteProviderId::WIRED_DISPLAY, kSinkId2);
mock_wired_display_provider_.SetRouteToReturn(CreateMediaRoute2());
// A request to |kSinkId| should only be sent to |mock_cast_provider_|.
EXPECT_CALL(mock_cast_provider_,
CreateRouteInternal(_, kSinkId, _, _, _, _, _))
.WillOnce(WithArg<6>(Invoke(
&mock_cast_provider_, &MockMediaRouteProvider::RouteRequestSuccess)));
EXPECT_CALL(mock_wired_display_provider_,
CreateRouteInternal(_, kSinkId, _, _, _, _, _))
.Times(0);
router()->CreateRoute(kSource, kSinkId, url::Origin::Create(GURL(kOrigin)),
nullptr, base::DoNothing(),
base::Milliseconds(kTimeoutMillis));
// A request to |kSinkId2| should only be sent to
// |mock_wired_display_provider_|.
EXPECT_CALL(mock_cast_provider_,
CreateRouteInternal(_, kSinkId2, _, _, _, _, _))
.Times(0);
EXPECT_CALL(mock_wired_display_provider_,
CreateRouteInternal(_, kSinkId2, _, _, _, _, _))
.WillOnce(
WithArg<6>(Invoke(&mock_wired_display_provider_,
&MockMediaRouteProvider::RouteRequestSuccess)));
router()->CreateRoute(kSource, kSinkId2, url::Origin::Create(GURL(kOrigin)),
nullptr, base::DoNothing(),
base::Milliseconds(kTimeoutMillis));
base::RunLoop().RunUntilIdle();
}
TEST_F(MediaRouterDesktopTest, SendRouteRequestsToMultipleProviders) {
// Register |mock_wired_display_provider_| with the router.
// |mock_cast_provider_| has already been registered during setup.
RegisterWiredDisplayProvider();
// Set |mock_cast_provider_| to have one route with ID |kRouteId|.
ProvideTestRoute(mojom::MediaRouteProviderId::CAST, kRouteId);
// Set |mock_wired_display_provider_| to have one route with ID |kRouteId2|.
ProvideTestRoute(mojom::MediaRouteProviderId::WIRED_DISPLAY, kRouteId2);
// A request for |kRouteId| should only be sent to |mock_cast_provider_|.
EXPECT_CALL(mock_cast_provider_, DetachRoute(kRouteId));
EXPECT_CALL(mock_wired_display_provider_, DetachRoute(kRouteId)).Times(0);
router()->DetachRoute(kRouteId);
// A request for |kRouteId2| should only be sent to
// |mock_wired_display_provider_|.
EXPECT_CALL(mock_cast_provider_, TerminateRouteInternal(kRouteId2, _))
.Times(0);
EXPECT_CALL(mock_wired_display_provider_,
TerminateRouteInternal(kRouteId2, _))
.WillOnce(
WithArg<1>(Invoke(&mock_wired_display_provider_,
&MockMediaRouteProvider::TerminateRouteSuccess)));
router()->TerminateRoute(kRouteId2);
base::RunLoop().RunUntilIdle();
}
TEST_F(MediaRouterDesktopTest, ObserveSinksFromMultipleProviders) {
const MediaSource source(kSource);
// Sinks for the Cast MRP.
MediaSinkInternal sink1a;
MediaSinkInternal sink1b;
sink1a.set_sink(MediaSink("sink1a", "", SinkIconType::CAST,
mojom::MediaRouteProviderId::CAST));
sink1b.set_sink(MediaSink("sink1b", "", SinkIconType::CAST,
mojom::MediaRouteProviderId::CAST));
// Sinks for the wired display MRP.
MediaSinkInternal sink2a;
MediaSinkInternal sink2b;
sink2a.set_sink(MediaSink("sink2a", "", SinkIconType::CAST,
mojom::MediaRouteProviderId::WIRED_DISPLAY));
sink2b.set_sink(MediaSink("sink2b", "", SinkIconType::CAST,
mojom::MediaRouteProviderId::WIRED_DISPLAY));
RegisterWiredDisplayProvider();
NiceMock<MockMediaSinksObserver> observer(router(), source,
url::Origin::Create(GURL(kOrigin)));
observer.Init();
// Have the Cast MRP report sinks.
EXPECT_CALL(observer, OnSinksReceived(UnorderedElementsAre(sink1a.sink(),
sink1b.sink())));
ReceiveSinks(mojom::MediaRouteProviderId::CAST, kSource, {sink1a, sink1b});
// Have the wired display MRP report sinks.
EXPECT_CALL(observer,
OnSinksReceived(UnorderedElementsAre(
sink1a.sink(), sink1b.sink(), sink2a.sink(), sink2b.sink())));
ReceiveSinks(mojom::MediaRouteProviderId::WIRED_DISPLAY, kSource,
{sink2a, sink2b});
// Have the Cast MRP report an empty list of sinks.
EXPECT_CALL(observer, OnSinksReceived(UnorderedElementsAre(sink2a.sink(),
sink2b.sink())));
ReceiveSinks(mojom::MediaRouteProviderId::CAST, kSource, {});
// Have the wired display MRP report an empty list of sinks.
EXPECT_CALL(observer, OnSinksReceived(IsEmpty()));
ReceiveSinks(mojom::MediaRouteProviderId::WIRED_DISPLAY, kSource, {});
}
TEST_F(MediaRouterDesktopTest, ObserveRoutesFromMultipleProviders) {
const MediaSource source(kSource);
// Routes for the Cast MRP.
const MediaRoute route1a("route1a", source, "sink 1a", "", true);
const MediaRoute route1b("route1b", source, "sink 1b", "", true);
// Routes for the wired display MRP.
const MediaRoute route2a("route2a", source, "sink 2a", "", true);
const MediaRoute route2b("route2b", source, "sink 2b", "", true);
RegisterWiredDisplayProvider();
MockMediaRoutesObserver observer(router());
// Have the Cast MRP report routes.
EXPECT_CALL(observer,
OnRoutesUpdated(UnorderedElementsAre(route1a, route1b)));
UpdateRoutes(mojom::MediaRouteProviderId::CAST, {route1a, route1b});
// Have the wired display MRP report routes.
EXPECT_CALL(observer, OnRoutesUpdated(UnorderedElementsAre(
route1a, route1b, route2a, route2b)));
UpdateRoutes(mojom::MediaRouteProviderId::WIRED_DISPLAY, {route2a, route2b});
// Have the Cast MRP report an empty list of routes.
EXPECT_CALL(observer,
OnRoutesUpdated(UnorderedElementsAre(route2a, route2b)));
UpdateRoutes(mojom::MediaRouteProviderId::CAST, {});
// Have the wired display MRP report an empty list of routes.
EXPECT_CALL(observer, OnRoutesUpdated(IsEmpty()));
UpdateRoutes(mojom::MediaRouteProviderId::WIRED_DISPLAY, {});
}
TEST_F(MediaRouterDesktopTest, TestRecordPresentationRequestUrlBySink) {
using base::Bucket;
using PresentationUrlBySink = MediaRouterDesktop::PresentationUrlBySink;
MediaSource cast_source("cast:ABCD1234");
MediaSource remote_playback_source(
"remote-playback:media-session?&video_codec=vp8&audio_codec=aac");
MediaSource dial_source(
GURL(base::StrCat({kCastDialPresentationUrlScheme, ":YouTube"})));
MediaSource presentation_url(GURL("https://www.example.com"));
base::HistogramTester tester;
RecordPresentationRequestUrlBySink(cast_source,
mojom::MediaRouteProviderId::CAST, 6);
RecordPresentationRequestUrlBySink(remote_playback_source,
mojom::MediaRouteProviderId::CAST, 5);
RecordPresentationRequestUrlBySink(dial_source,
mojom::MediaRouteProviderId::DIAL, 4);
RecordPresentationRequestUrlBySink(presentation_url,
mojom::MediaRouteProviderId::CAST, 3);
RecordPresentationRequestUrlBySink(
presentation_url, mojom::MediaRouteProviderId::WIRED_DISPLAY, 2);
// DIAL devices don't support normal URLs, so this will get logged as
// kUnknown.
RecordPresentationRequestUrlBySink(presentation_url,
mojom::MediaRouteProviderId::DIAL, 1);
EXPECT_THAT(
tester.GetAllSamples("MediaRouter.PresentationRequest.UrlBySink2"),
testing::UnorderedElementsAre(
Bucket(static_cast<int>(PresentationUrlBySink::kUnknown), 1),
Bucket(
static_cast<int>(PresentationUrlBySink::kNormalUrlToWiredDisplay),
2),
Bucket(
static_cast<int>(PresentationUrlBySink::kNormalUrlToChromecast),
3),
Bucket(static_cast<int>(PresentationUrlBySink::kDialUrlToDial), 4),
Bucket(static_cast<int>(PresentationUrlBySink::kRemotePlayback), 5),
Bucket(static_cast<int>(PresentationUrlBySink::kCastUrlToChromecast),
6)));
}
TEST_F(MediaRouterDesktopTest, TestGetCurrentRoutes) {
MediaSource source1("source_1");
MediaSource source2("source_1");
MediaRoute route1("route_1", source1, "sink_1", "", false);
MediaRoute route2("route_2", source2, "sink_2", "", true);
std::vector<MediaRoute> routes = {route1, route2};
EXPECT_TRUE(router()->GetCurrentRoutes().empty());
router()->current_routes_ = routes;
std::vector<MediaRoute> current_routes = router()->GetCurrentRoutes();
ASSERT_EQ(current_routes.size(), 2u);
EXPECT_EQ(current_routes[0], route1);
EXPECT_EQ(current_routes[1], route2);
router()->current_routes_ = std::vector<MediaRoute>();
EXPECT_TRUE(router()->GetCurrentRoutes().empty());
}
TEST_F(MediaRouterDesktopTest, GetMirroringMediaControllerHost) {
MediaSource tab_source(kTabSourceOne);
auto local_mirroring_route =
MediaRoute(kRouteId, tab_source, kSinkId, kDescription, true);
local_mirroring_route.set_controller_type(RouteControllerType::kGeneric);
std::vector<MediaRoute> local_mirroring_routes{local_mirroring_route};
EXPECT_CALL(mock_cast_provider_,
BindMediaControllerInternal(kRouteId, _, _, _))
.WillOnce(
[&](const std::string& route_id,
mojo::PendingReceiver<mojom::MediaController>& media_controller,
mojo::PendingRemote<mojom::MediaStatusObserver>& observer,
MockMediaRouteProvider::BindMediaControllerCallback& callback) {
std::move(callback).Run(true);
});
UpdateRoutes(mojom::MediaRouteProviderId::CAST, local_mirroring_routes);
base::RunLoop().RunUntilIdle();
// Expect the host to exist for a local mirroring source.
EXPECT_NE(nullptr, router()->GetMirroringMediaControllerHost(kRouteId));
std::vector<MediaRoute> nonlocal_mirroring_routes{
MediaRoute(kRouteId2, tab_source, kSinkId, kDescription, false)};
UpdateRoutes(mojom::MediaRouteProviderId::CAST, nonlocal_mirroring_routes);
// Expect that the host for kRouteId no longer exists.
EXPECT_EQ(nullptr, router()->GetMirroringMediaControllerHost(kRouteId));
// Expect that no host for kRouteId2 exists, as it is not a local source.
EXPECT_EQ(nullptr, router()->GetMirroringMediaControllerHost(kRouteId2));
}
TEST_F(MediaRouterDesktopTest, OnUserGesture) {
auto media_sink_service = std::make_unique<MockDualMediaSinkService>();
SetMockSinkServiceForTest(media_sink_service.get());
// If Cast and DIAL discovery hasn't started yet, user gesture will start
// discovery.
EXPECT_CALL(*media_sink_service, StartDiscovery);
EXPECT_CALL(*media_sink_service, DiscoverSinksNow).Times(0);
OnUserGesture();
// If Cast and DIAL discovery has started, user gesture will not lead to
// starting discovery again. Instead, the sink service will request to start a
// new discovery cycle.
ON_CALL(*media_sink_service, MdnsDiscoveryStarted)
.WillByDefault(Return(true));
ON_CALL(*media_sink_service, DialDiscoveryStarted)
.WillByDefault(Return(true));
EXPECT_CALL(*media_sink_service, StartDiscovery).Times(0);
EXPECT_CALL(*media_sink_service, DiscoverSinksNow);
OnUserGesture();
// Clean up
SetMockSinkServiceForTest(nullptr);
}
} // namespace media_router