Commit 23a0140b authored by Derek Cheng's avatar Derek Cheng Committed by Commit Bot

[Cast MRP] In-browser Cast MRP part 2a.

Add utility class that handles messages between a Cast SDK client and
the Cast MRP ("internal messages"). The types of internal messages are:
- new session (mrp -> client)
- receiver action (mrp -> client)
- client connect (client -> mrp)
- app message (client <-> mrp)

In particular, client connect should be going away soon (with the use
of PresentationConnection in MRP). app message is bi-directional between
the client and the receiver device, where MRP acts as a pass through.


Also added a utility method to generate a stop session cast channel
message for a given receiver device and session id.


Bug: 809249
Change-Id: I62169dd5b163060464cc6d389c8d93fc4bb2425f
Reviewed-on: https://chromium-review.googlesource.com/1085768Reviewed-by: default avatarBernhard Bauer <bauerb@chromium.org>
Reviewed-by: default avatarBrandon Tolsch <btolsch@chromium.org>
Commit-Queue: Derek Cheng <imcheng@chromium.org>
Cr-Commit-Position: refs/heads/master@{#577506}
parent 34cab752
...@@ -14,6 +14,7 @@ static_library("router") { ...@@ -14,6 +14,7 @@ static_library("router") {
"//components/keyed_service/core", "//components/keyed_service/core",
"//content/public/browser", "//content/public/browser",
"//content/public/common", "//content/public/common",
"//crypto",
"//net", "//net",
"//third_party/icu", "//third_party/icu",
"//url", "//url",
...@@ -93,6 +94,8 @@ static_library("router") { ...@@ -93,6 +94,8 @@ static_library("router") {
"providers/cast/cast_app_availability_tracker.h", "providers/cast/cast_app_availability_tracker.h",
"providers/cast/cast_app_discovery_service.cc", "providers/cast/cast_app_discovery_service.cc",
"providers/cast/cast_app_discovery_service.h", "providers/cast/cast_app_discovery_service.h",
"providers/cast/cast_internal_message_util.cc",
"providers/cast/cast_internal_message_util.h",
"providers/cast/cast_media_route_provider.cc", "providers/cast/cast_media_route_provider.cc",
"providers/cast/cast_media_route_provider.h", "providers/cast/cast_media_route_provider.h",
"providers/cast/cast_media_route_provider_metrics.cc", "providers/cast/cast_media_route_provider_metrics.cc",
......
...@@ -4,10 +4,13 @@ ...@@ -4,10 +4,13 @@
#include "chrome/browser/media/router/media_router_feature.h" #include "chrome/browser/media/router/media_router_feature.h"
#include "base/base64.h"
#include "base/feature_list.h" #include "base/feature_list.h"
#include "base/strings/string_util.h"
#include "build/build_config.h" #include "build/build_config.h"
#include "chrome/browser/profiles/profile.h" #include "chrome/browser/profiles/profile.h"
#include "content/public/browser/browser_context.h" #include "content/public/browser/browser_context.h"
#include "crypto/random.h"
#include "extensions/buildflags/buildflags.h" #include "extensions/buildflags/buildflags.h"
#include "ui/base/ui_features.h" #include "ui/base/ui_features.h"
...@@ -73,6 +76,12 @@ void RegisterLocalStatePrefs(PrefRegistrySimple* registry) { ...@@ -73,6 +76,12 @@ void RegisterLocalStatePrefs(PrefRegistrySimple* registry) {
PrefRegistry::PUBLIC); PrefRegistry::PUBLIC);
} }
void RegisterProfilePrefs(PrefRegistrySimple* registry) {
// TODO(imcheng): Migrate existing Media Router prefs to here.
registry->RegisterStringPref(prefs::kMediaRouterReceiverIdHashToken, "",
PrefRegistry::PUBLIC);
}
const base::Feature kCastAllowAllIPsFeature{"CastAllowAllIPs", const base::Feature kCastAllowAllIPsFeature{"CastAllowAllIPs",
base::FEATURE_DISABLED_BY_DEFAULT}; base::FEATURE_DISABLED_BY_DEFAULT};
...@@ -90,6 +99,19 @@ bool GetCastAllowAllIPsPref(PrefService* pref_service) { ...@@ -90,6 +99,19 @@ bool GetCastAllowAllIPsPref(PrefService* pref_service) {
return allow_all_ips; return allow_all_ips;
} }
std::string GetReceiverIdHashToken(PrefService* pref_service) {
static constexpr size_t kHashTokenSize = 64;
std::string token =
pref_service->GetString(prefs::kMediaRouterReceiverIdHashToken);
if (token.empty()) {
crypto::RandBytes(base::WriteInto(&token, kHashTokenSize + 1),
kHashTokenSize);
base::Base64Encode(token, &token);
pref_service->SetString(prefs::kMediaRouterReceiverIdHashToken, token);
}
return token;
}
bool DialMediaRouteProviderEnabled() { bool DialMediaRouteProviderEnabled() {
return base::FeatureList::IsEnabled(kDialMediaRouteProvider); return base::FeatureList::IsEnabled(kDialMediaRouteProvider);
} }
......
...@@ -25,11 +25,18 @@ namespace prefs { ...@@ -25,11 +25,18 @@ namespace prefs {
// Pref name for the enterprise policy for allowing Cast devices on all IPs. // Pref name for the enterprise policy for allowing Cast devices on all IPs.
constexpr char kMediaRouterCastAllowAllIPs[] = constexpr char kMediaRouterCastAllowAllIPs[] =
"media_router.cast_allow_all_ips"; "media_router.cast_allow_all_ips";
// Pref name for the per-profile randomly generated token to include with the
// hash when externalizing MediaSink IDs.
constexpr char kMediaRouterReceiverIdHashToken[] =
"media_router.receiver_id_hash_token";
} // namespace prefs } // namespace prefs
// Registers |kMediaRouterCastAllowAllIPs| with local state pref |registry|. // Registers |kMediaRouterCastAllowAllIPs| with local state pref |registry|.
void RegisterLocalStatePrefs(PrefRegistrySimple* registry); void RegisterLocalStatePrefs(PrefRegistrySimple* registry);
// Registers Media Router related preferences with per-profile pref |registry|.
void RegisterProfilePrefs(PrefRegistrySimple* registry);
// If enabled, allows Media Router to connect to Cast devices on all IP // If enabled, allows Media Router to connect to Cast devices on all IP
// addresses, not just RFC1918/RFC4913 private addresses. Workaround for // addresses, not just RFC1918/RFC4913 private addresses. Workaround for
// https://crbug.com/813974. // https://crbug.com/813974.
...@@ -39,6 +46,11 @@ extern const base::Feature kCastAllowAllIPsFeature; ...@@ -39,6 +46,11 @@ extern const base::Feature kCastAllowAllIPsFeature;
// all IPs, as determined by local state |pref_service| / feature flag. // all IPs, as determined by local state |pref_service| / feature flag.
bool GetCastAllowAllIPsPref(PrefService* pref_service); bool GetCastAllowAllIPsPref(PrefService* pref_service);
// Returns the hash token to use for externalizing MediaSink IDs from
// |pref_service|. If the token does not exist, the token will be created from a
// randomly generated string and stored in |pref_service|.
std::string GetReceiverIdHashToken(PrefService* pref_service);
extern const base::Feature kEnableDialSinkQuery; extern const base::Feature kEnableDialSinkQuery;
extern const base::Feature kEnableCastDiscovery; extern const base::Feature kEnableCastDiscovery;
extern const base::Feature kCastMediaRouteProvider; extern const base::Feature kCastMediaRouteProvider;
......
...@@ -30,4 +30,16 @@ TEST(MediaRouterFeatureTest, GetCastAllowAllIPsPref) { ...@@ -30,4 +30,16 @@ TEST(MediaRouterFeatureTest, GetCastAllowAllIPsPref) {
EXPECT_FALSE(GetCastAllowAllIPsPref(pref_service.get())); EXPECT_FALSE(GetCastAllowAllIPsPref(pref_service.get()));
} }
TEST(MediaRouterFeatureTest, GetReceiverIdHashToken) {
auto pref_service = std::make_unique<TestingPrefServiceSimple>();
pref_service->registry()->RegisterStringPref(
prefs::kMediaRouterReceiverIdHashToken, "");
std::string token = GetReceiverIdHashToken(pref_service.get());
EXPECT_FALSE(token.empty());
// Token stays the same on subsequent invocation.
EXPECT_EQ(token, GetReceiverIdHashToken(pref_service.get()));
}
} // namespace media_router } // namespace media_router
...@@ -76,7 +76,7 @@ TEST_F(CastAppAvailabilityTrackerTest, RegisterSourceReturnsMultipleAppIds) { ...@@ -76,7 +76,7 @@ TEST_F(CastAppAvailabilityTrackerTest, RegisterSourceReturnsMultipleAppIds) {
TEST_F(CastAppAvailabilityTrackerTest, MultipleAppIdsAlreadyTrackingOne) { TEST_F(CastAppAvailabilityTrackerTest, MultipleAppIdsAlreadyTrackingOne) {
// One of the mirroring app IDs. // One of the mirroring app IDs.
auto source1 = CastMediaSource::From("cast:0F5096E8"); auto source1 = CastMediaSource::From("cast:0F5096E8?clientId=123");
ASSERT_TRUE(source1); ASSERT_TRUE(source1);
base::flat_set<std::string> new_app_ids = {"0F5096E8"}; base::flat_set<std::string> new_app_ids = {"0F5096E8"};
......
// Copyright 2018 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/router/providers/cast/cast_internal_message_util.h"
#include "base/base64url.h"
#include "base/json/json_writer.h"
#include "base/memory/ptr_util.h"
#include "base/sha1.h"
#include "chrome/common/media_router/discovery/media_sink_internal.h"
#include "chrome/common/media_router/providers/cast/cast_media_source.h"
#include "components/cast_channel/cast_socket.h"
#include "components/cast_channel/proto/cast_channel.pb.h"
#include "net/base/escape.h"
namespace media_router {
namespace {
constexpr char kClientConnect[] = "client_connect";
constexpr char kAppMessage[] = "app_message";
constexpr char kReceiverAction[] = "receiver_action";
constexpr char kNewSession[] = "new_session";
bool GetString(const base::Value& value,
const std::string& key,
std::string* out) {
const base::Value* string_value =
value.FindKeyOfType(key, base::Value::Type::STRING);
if (!string_value)
return false;
*out = string_value->GetString();
return !out->empty();
}
void CopyValueWithDefault(const base::Value& from,
const std::string& key,
base::Value default_value,
base::Value* to) {
const base::Value* value = from.FindKey(key);
to->SetKey(key, value ? value->Clone() : std::move(default_value));
}
void CopyValue(const base::Value& from,
const std::string& key,
base::Value* to) {
const base::Value* value = from.FindKey(key);
if (value)
to->SetKey(key, value->Clone());
}
CastInternalMessage::Type CastInternalMessageTypeFromString(
const std::string& type) {
if (type == kClientConnect)
return CastInternalMessage::Type::kClientConnect;
if (type == kAppMessage)
return CastInternalMessage::Type::kAppMessage;
if (type == kReceiverAction)
return CastInternalMessage::Type::kReceiverAction;
if (type == kNewSession)
return CastInternalMessage::Type::kNewSession;
return CastInternalMessage::Type::kOther;
}
std::string CastInternalMessageTypeToString(CastInternalMessage::Type type) {
switch (type) {
case CastInternalMessage::Type::kClientConnect:
return kClientConnect;
case CastInternalMessage::Type::kAppMessage:
return kAppMessage;
case CastInternalMessage::Type::kReceiverAction:
return kReceiverAction;
case CastInternalMessage::Type::kNewSession:
return kNewSession;
case CastInternalMessage::Type::kOther:
NOTREACHED();
return "";
}
NOTREACHED();
return "";
}
// Possible types in a receiver_action message.
constexpr char kReceiverActionTypeCast[] = "cast";
constexpr char kReceiverActionTypeStop[] = "stop";
base::ListValue CapabilitiesToListValue(uint8_t capabilities) {
base::ListValue value;
auto& storage = value.GetList();
if (capabilities & cast_channel::VIDEO_OUT)
storage.emplace_back("video_out");
if (capabilities & cast_channel::VIDEO_IN)
storage.emplace_back("video_in");
if (capabilities & cast_channel::AUDIO_OUT)
storage.emplace_back("audio_out");
if (capabilities & cast_channel::AUDIO_IN)
storage.emplace_back("audio_in");
if (capabilities & cast_channel::MULTIZONE_GROUP)
storage.emplace_back("multizone_group");
return value;
}
base::Value CreateReceiver(const MediaSinkInternal& sink,
const std::string& hash_token) {
base::Value receiver(base::Value::Type::DICTIONARY);
std::string label = base::SHA1HashString(sink.sink().id() + hash_token);
base::Base64UrlEncode(label, base::Base64UrlEncodePolicy::OMIT_PADDING,
&label);
receiver.SetKey("label", base::Value(label));
receiver.SetKey("friendlyName",
base::Value(net::EscapeForHTML(sink.sink().name())));
receiver.SetKey("capabilities",
CapabilitiesToListValue(sink.cast_data().capabilities));
receiver.SetKey("volume", base::Value());
receiver.SetKey("isActiveInput", base::Value());
receiver.SetKey("displayStatus", base::Value());
receiver.SetKey("receiverType", base::Value("cast"));
return receiver;
}
base::Value CreateReceiverActionMessage(const std::string& client_id,
const MediaSinkInternal& sink,
const std::string& hash_token,
const char* action_type) {
base::Value message(base::Value::Type::DICTIONARY);
message.SetKey("receiver", CreateReceiver(sink, hash_token));
message.SetKey("action", base::Value(action_type));
base::Value value(base::Value::Type::DICTIONARY);
value.SetKey("type", base::Value(CastInternalMessageTypeToString(
CastInternalMessage::Type::kReceiverAction)));
value.SetKey("message", std::move(message));
value.SetKey("sequenceNumber", base::Value(-1));
value.SetKey("timeoutMillis", base::Value(0));
value.SetKey("clientId", base::Value(client_id));
return value;
}
base::Value CreateAppMessageBody(
const std::string& session_id,
const cast_channel::CastMessage& cast_message) {
// TODO(https://crbug.com/862532): Investigate whether it is possible to move
// instead of copying the contents of |cast_message|. Right now copying is
// done because the message is passed as a const ref at the
// CastSocket::Observer level.
base::Value message(base::Value::Type::DICTIONARY);
message.SetKey("sessionId", base::Value(session_id));
message.SetKey("namespaceName", base::Value(cast_message.namespace_()));
switch (cast_message.payload_type()) {
case cast_channel::CastMessage_PayloadType_STRING:
message.SetKey("message", base::Value(cast_message.payload_utf8()));
break;
case cast_channel::CastMessage_PayloadType_BINARY: {
const auto& payload = cast_message.payload_binary();
message.SetKey("message",
base::Value(base::Value::BlobStorage(
payload.front(), payload.front() + payload.size())));
break;
}
default:
NOTREACHED();
break;
}
return message;
}
} // namespace
// static
std::unique_ptr<CastInternalMessage> CastInternalMessage::From(
base::Value message) {
if (!message.is_dict()) {
DVLOG(2) << "Failed to read JSON message: " << message;
return nullptr;
}
std::string str_type;
if (!GetString(message, "type", &str_type)) {
DVLOG(2) << "Missing type value, message: " << message;
return nullptr;
}
CastInternalMessage::Type message_type =
CastInternalMessageTypeFromString(str_type);
if (message_type == CastInternalMessage::Type::kOther) {
DVLOG(2) << __func__ << ": Unsupported message type: " << str_type
<< ", message: " << message;
return nullptr;
}
std::string client_id;
if (!GetString(message, "clientId", &client_id)) {
DVLOG(2) << "Missing clientId, message: " << message;
return nullptr;
}
base::Value* message_body_value = message.FindKey("message");
if (!message_body_value ||
(!message_body_value->is_dict() && !message_body_value->is_string())) {
DVLOG(2) << "Missing message body, message: " << message;
return nullptr;
}
auto internal_message =
std::make_unique<CastInternalMessage>(message_type, client_id);
base::Value* sequence_number_value =
message.FindKeyOfType("sequenceNumber", base::Value::Type::INTEGER);
if (sequence_number_value)
internal_message->sequence_number = sequence_number_value->GetInt();
if (message_type == CastInternalMessage::Type::kAppMessage) {
if (!message_body_value->is_dict())
return nullptr;
if (!GetString(*message_body_value, "namespaceName",
&internal_message->app_message_namespace) ||
!GetString(*message_body_value, "sessionId",
&internal_message->app_message_session_id)) {
DVLOG(2) << "Missing namespace or session ID, message: " << message;
return nullptr;
}
base::Value* app_message_value = message_body_value->FindKey("message");
if (!app_message_value ||
(!app_message_value->is_dict() && !app_message_value->is_string())) {
DVLOG(2) << "Missing app message, message: " << message;
return nullptr;
}
internal_message->app_message_body = std::move(*app_message_value);
}
return internal_message;
}
CastInternalMessage::CastInternalMessage(CastInternalMessage::Type type,
const std::string& client_id)
: type(type), client_id(client_id) {}
CastInternalMessage::~CastInternalMessage() = default;
blink::mojom::PresentationConnectionMessagePtr
CreatePresentationConnectionMessage(const base::Value& message) {
std::string str;
CHECK(base::JSONWriter::Write(message, &str));
return blink::mojom::PresentationConnectionMessage::NewMessage(str);
}
blink::mojom::PresentationConnectionMessagePtr CreateReceiverActionCastMessage(
const std::string& client_id,
const MediaSinkInternal& sink,
const std::string& hash_token) {
return CreatePresentationConnectionMessage(CreateReceiverActionMessage(
client_id, sink, hash_token, kReceiverActionTypeCast));
}
blink::mojom::PresentationConnectionMessagePtr CreateReceiverActionStopMessage(
const std::string& client_id,
const MediaSinkInternal& sink,
const std::string& hash_token) {
return CreatePresentationConnectionMessage(CreateReceiverActionMessage(
client_id, sink, hash_token, kReceiverActionTypeStop));
}
// static
std::unique_ptr<CastSession> CastSession::From(
const MediaSinkInternal& sink,
const std::string& hash_token,
const base::Value& receiver_status) {
// There should be only 1 app on |receiver_status|.
const base::Value* app_list_value =
receiver_status.FindKeyOfType("applications", base::Value::Type::LIST);
if (!app_list_value || app_list_value->GetList().size() != 1) {
DVLOG(2) << "receiver_status does not contain exactly one app: "
<< receiver_status;
return nullptr;
}
auto session = std::make_unique<CastSession>();
// Fill in mandatory Session fields.
const base::Value& app_value = app_list_value->GetList()[0];
if (!GetString(app_value, "sessionId", &session->session_id) ||
!GetString(app_value, "appId", &session->app_id) ||
!GetString(app_value, "transportId", &session->transport_id) ||
!GetString(app_value, "displayName", &session->display_name)) {
DVLOG(2) << "app_value missing mandatory fields: " << app_value;
return nullptr;
}
// Optional Session fields.
GetString(app_value, "statusText", &session->status);
base::Value receiver_value = CreateReceiver(sink, hash_token);
CopyValue(receiver_status, "volume", &receiver_value);
CopyValue(receiver_status, "isActiveInput", &receiver_value);
// Create |session->value|.
session->value = base::Value(base::Value::Type::DICTIONARY);
auto& session_value = session->value;
session_value.SetKey("sessionId", base::Value(session->session_id));
session_value.SetKey("appId", base::Value(session->app_id));
session_value.SetKey("transportId", base::Value(session->transport_id));
session_value.SetKey("receiver", std::move(receiver_value));
CopyValueWithDefault(app_value, "displayName", base::Value(""),
&session_value);
CopyValueWithDefault(app_value, "senderApps", base::ListValue(),
&session_value);
CopyValueWithDefault(app_value, "statusText", base::Value(), &session_value);
CopyValueWithDefault(app_value, "appImages", base::ListValue(),
&session_value);
CopyValueWithDefault(app_value, "namespaces", base::ListValue(),
&session_value);
const base::Value* namespaces_value =
app_value.FindKeyOfType("namespaces", base::Value::Type::LIST);
if (!namespaces_value || namespaces_value->GetList().empty()) {
// A session without namespaces is invalid, except for a multizone leader.
if (session->app_id != kMultizoneLeaderAppId)
return nullptr;
} else {
for (const auto& namespace_value : namespaces_value->GetList()) {
std::string message_namespace;
if (!namespace_value.is_dict() ||
!GetString(namespace_value, "name", &message_namespace))
return nullptr;
session->message_namespaces.insert(std::move(message_namespace));
}
}
session_value.SetKey("namespaces",
namespaces_value ? namespaces_value->Clone()
: base::Value(base::Value::Type::LIST));
return session;
}
CastSession::CastSession() = default;
CastSession::~CastSession() = default;
// static
std::string CastSession::GetRouteDescription(const CastSession& session) {
return !session.status.empty() ? session.status : session.display_name;
}
blink::mojom::PresentationConnectionMessagePtr CreateNewSessionMessage(
const CastSession& session,
const std::string& client_id) {
base::Value message(base::Value::Type::DICTIONARY);
message.SetKey("type", base::Value(CastInternalMessageTypeToString(
CastInternalMessage::Type::kNewSession)));
message.SetKey("message", session.value.Clone());
message.SetKey("sequenceNumber", base::Value(-1));
message.SetKey("timeoutMillis", base::Value(0));
message.SetKey("clientId", base::Value(client_id));
return CreatePresentationConnectionMessage(message);
}
blink::mojom::PresentationConnectionMessagePtr CreateAppMessageAck(
const std::string& client_id,
int sequence_number) {
base::Value message(base::Value::Type::DICTIONARY);
message.SetKey("type", base::Value(CastInternalMessageTypeToString(
CastInternalMessage::Type::kAppMessage)));
message.SetKey("message", base::Value());
message.SetKey("sequenceNumber", base::Value(sequence_number));
message.SetKey("timeoutMillis", base::Value(0));
message.SetKey("clientId", base::Value(client_id));
return CreatePresentationConnectionMessage(message);
}
blink::mojom::PresentationConnectionMessagePtr CreateAppMessage(
const std::string& session_id,
const std::string& client_id,
const cast_channel::CastMessage& cast_message) {
base::Value message(base::Value::Type::DICTIONARY);
message.SetKey("type", base::Value(CastInternalMessageTypeToString(
CastInternalMessage::Type::kAppMessage)));
message.SetKey("message", CreateAppMessageBody(session_id, cast_message));
message.SetKey("sequenceNumber", base::Value(-1));
message.SetKey("timeoutMillis", base::Value(0));
message.SetKey("clientId", base::Value(client_id));
return CreatePresentationConnectionMessage(message);
}
} // namespace media_router
// Copyright 2018 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.
#ifndef CHROME_BROWSER_MEDIA_ROUTER_PROVIDERS_CAST_CAST_INTERNAL_MESSAGE_UTIL_H_
#define CHROME_BROWSER_MEDIA_ROUTER_PROVIDERS_CAST_CAST_INTERNAL_MESSAGE_UTIL_H_
#include "base/containers/flat_set.h"
#include "base/macros.h"
#include "base/values.h"
#include "third_party/blink/public/platform/modules/presentation/presentation.mojom.h"
namespace cast_channel {
class CastMessage;
}
namespace media_router {
class MediaSinkInternal;
// Represents a message sent or received by the Cast SDK via a
// PresentationConnection.
struct CastInternalMessage {
// TODO(crbug.com/809249): Add other types of messages.
enum class Type {
kClientConnect, // Initial message sent by SDK client to connect to MRP.
kAppMessage, // App messages to pass through between SDK client and the
// receiver.
kReceiverAction, // Message sent by MRP to inform SDK client of action.
kNewSession, // Message sent by MRP to inform SDK client of new
// session.
kOther // All other types of messages which are not considered
// part of communication with Cast SDK.
};
// Returns a CastInternalMessage for |message|, or nullptr is |message| is not
// a valid Cast internal message.
static std::unique_ptr<CastInternalMessage> From(base::Value message);
CastInternalMessage(Type type, const std::string& client_id);
~CastInternalMessage();
Type type;
std::string client_id;
int sequence_number = -1;
// The following are set if |type| is |kAppMessage|.
std::string app_message_namespace;
std::string app_message_session_id;
base::Value app_message_body;
DISALLOW_COPY_AND_ASSIGN(CastInternalMessage);
};
// Represents a Cast session on a Cast device. Cast sessions are derived from
// RECEIVER_STATUS messages sent by Cast devices.
class CastSession {
public:
// Returns a CastSession from |receiver_status| message sent by |sink|, or
// nullptr if |receiver_status| is not a valid RECEIVER_STATUS message.
// |hash_token| is a per-profile value that is used to hash the sink ID.
static std::unique_ptr<CastSession> From(const MediaSinkInternal& sink,
const std::string& hash_token,
const base::Value& receiver_status);
// Returns a string that can be used as the description of the MediaRoute
// associated with this session.
static std::string GetRouteDescription(const CastSession& session);
CastSession();
~CastSession();
// ID of the session.
std::string session_id;
// ID of the app in the session.
std::string app_id;
// ID used for communicating with the session over the Cast channel.
std::string transport_id;
// The set of accepted message namespaces. Must be non-empty, unless the
// session represents a multizone leader.
base::flat_set<std::string> message_namespaces;
// The human-readable name of the Cast application, for example, "YouTube".
// Mandatory.
std::string display_name;
// Descriptive text for the current application content, for example “My
// Wedding Slideshow”. May be empty.
std::string status;
// The dictionary representing this session, derived from |receiver_status|.
// For convenience, this is used for generating messages sent to the SDK that
// include the session value.
base::Value value;
};
// Utility methods for generating messages sent to the SDK.
// |hash_token| is a per-profile value that is used to hash the sink ID.
blink::mojom::PresentationConnectionMessagePtr CreateReceiverActionCastMessage(
const std::string& client_id,
const MediaSinkInternal& sink,
const std::string& hash_token);
blink::mojom::PresentationConnectionMessagePtr CreateReceiverActionStopMessage(
const std::string& client_id,
const MediaSinkInternal& sink,
const std::string& hash_token);
blink::mojom::PresentationConnectionMessagePtr CreateNewSessionMessage(
const CastSession& session,
const std::string& client_id);
blink::mojom::PresentationConnectionMessagePtr CreateAppMessageAck(
const std::string& client_id,
int sequence_number);
blink::mojom::PresentationConnectionMessagePtr CreateAppMessage(
const std::string& session_id,
const std::string& client_id,
const cast_channel::CastMessage& cast_message);
} // namespace media_router
#endif // CHROME_BROWSER_MEDIA_ROUTER_PROVIDERS_CAST_CAST_INTERNAL_MESSAGE_UTIL_H_
// Copyright 2018 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/router/providers/cast/cast_internal_message_util.h"
#include "base/json/json_reader.h"
#include "chrome/browser/media/router/test/test_helper.h"
#include "chrome/common/media_router/test/test_helper.h"
#include "components/cast_channel/cast_test_util.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace media_router {
namespace {
static constexpr char kReceiverIdToken[] = "token";
std::unique_ptr<base::Value> ReceiverStatus() {
std::string receiver_status_str = R"({
"applications": [{
"appId": "ABCDEFGH",
"displayName": "App display name",
"namespaces": [
{"name": "urn:x-cast:com.google.cast.media"},
{"name": "urn:x-cast:com.google.foo"}
],
"sessionId": "sessionId",
"statusText":"App status",
"transportId":"transportId"
}]
})";
return base::JSONReader::Read(receiver_status_str);
}
void ExpectNoCastSession(const MediaSinkInternal& sink,
const std::string& receiver_status_str,
const std::string& reason) {
auto receiver_status = base::JSONReader::Read(receiver_status_str);
ASSERT_TRUE(receiver_status);
auto session = CastSession::From(sink, kReceiverIdToken, *receiver_status);
EXPECT_FALSE(session) << "Shouldn't have created session because of "
<< reason;
}
void ExpectJSONMessagesEqual(const std::string& expected_message,
const std::string& message) {
auto expected_message_value = base::JSONReader::Read(expected_message);
ASSERT_TRUE(expected_message_value);
auto message_value = base::JSONReader::Read(message);
ASSERT_TRUE(message_value);
EXPECT_EQ(*expected_message_value, *message_value);
}
void ExpectInvalidCastInternalMessage(const std::string& message_str,
const std::string& invalid_reason) {
auto message_value = base::JSONReader::Read(message_str);
ASSERT_TRUE(message_value);
EXPECT_FALSE(CastInternalMessage::From(std::move(*message_value)))
<< "message expected to be invlaid: " << invalid_reason;
}
} // namespace
TEST(CastInternalMessageUtilTest, CastInternalMessageFromAppMessageString) {
std::string message_str = R"({
"type": "app_message",
"clientId": "12345",
"sequenceNumber": 999,
"message": {
"namespaceName": "urn:x-cast:com.google.foo",
"sessionId": "sessionId",
"message": { "foo": "bar" }
}
})";
auto message_value = base::JSONReader::Read(message_str);
ASSERT_TRUE(message_value);
auto message = CastInternalMessage::From(std::move(*message_value));
ASSERT_TRUE(message);
EXPECT_EQ(CastInternalMessage::Type::kAppMessage, message->type);
EXPECT_EQ("12345", message->client_id);
EXPECT_EQ(999, message->sequence_number);
EXPECT_EQ("urn:x-cast:com.google.foo", message->app_message_namespace);
EXPECT_EQ("sessionId", message->app_message_session_id);
base::Value message_body(base::Value::Type::DICTIONARY);
message_body.SetKey("foo", base::Value("bar"));
EXPECT_EQ(message_body, message->app_message_body);
}
TEST(CastInternalMessageUtilTest, CastInternalMessageFromClientConnectString) {
std::string message_str = R"({
"type": "client_connect",
"clientId": "12345",
"message": {}
})";
auto message_value = base::JSONReader::Read(message_str);
ASSERT_TRUE(message_value);
auto message = CastInternalMessage::From(std::move(*message_value));
ASSERT_TRUE(message);
EXPECT_EQ(CastInternalMessage::Type::kClientConnect, message->type);
EXPECT_EQ("12345", message->client_id);
EXPECT_EQ(-1, message->sequence_number);
EXPECT_TRUE(message->app_message_namespace.empty());
EXPECT_TRUE(message->app_message_session_id.empty());
EXPECT_EQ(base::Value(), message->app_message_body);
}
TEST(CastInternalMessageUtilTest, CastInternalMessageFromInvalidStrings) {
std::string unknown_type = R"({
"type": "some_unknown_type",
"clientId": "12345",
"message": {}
})";
ExpectInvalidCastInternalMessage(unknown_type, "unknown_type");
std::string missing_client_id = R"({
"type": "client_connect",
"message": {}
})";
ExpectInvalidCastInternalMessage(missing_client_id, "missing client ID");
std::string missing_message = R"({
"type": "client_connect",
"clientId": "12345"
})";
ExpectInvalidCastInternalMessage(missing_message, "missing message");
std::string app_message_missing_namespace = R"({
"type": "app_message",
"clientId": "12345",
"sequenceNumber": 999,
"message": {
"sessionId": "sessionId",
"message": { "foo": "bar" }
}
})";
ExpectInvalidCastInternalMessage(app_message_missing_namespace,
"missing namespace");
std::string app_message_missing_session_id = R"({
"type": "app_message",
"clientId": "12345",
"sequenceNumber": 999,
"message": {
"namespaceName": "urn:x-cast:com.google.foo",
"message": { "foo": "bar" }
}
})";
ExpectInvalidCastInternalMessage(app_message_missing_session_id,
"missing session ID");
std::string app_message_missing_message = R"({
"type": "app_message",
"clientId": "12345",
"sequenceNumber": 999,
"message": {
"namespaceName": "urn:x-cast:com.google.foo",
"sessionId": "sessionId"
}
})";
ExpectInvalidCastInternalMessage(app_message_missing_message,
"missing app message");
}
TEST(CastInternalMessageUtilTest, CastSessionFromReceiverStatusNoStatusText) {
MediaSinkInternal sink = CreateCastSink(1);
std::string receiver_status_str = R"({
"applications": [{
"appId": "ABCDEFGH",
"displayName": "App display name",
"namespaces": [
{"name": "urn:x-cast:com.google.cast.media"},
{"name": "urn:x-cast:com.google.foo"}
],
"sessionId": "sessionId",
"transportId":"transportId"
}]
})";
auto receiver_status = base::JSONReader::Read(receiver_status_str);
ASSERT_TRUE(receiver_status);
auto session = CastSession::From(sink, kReceiverIdToken, *receiver_status);
ASSERT_TRUE(session);
EXPECT_EQ("sessionId", session->session_id);
EXPECT_EQ("ABCDEFGH", session->app_id);
EXPECT_EQ("transportId", session->transport_id);
base::flat_set<std::string> message_namespaces = {
"urn:x-cast:com.google.cast.media", "urn:x-cast:com.google.foo"};
EXPECT_EQ(message_namespaces, session->message_namespaces);
EXPECT_TRUE(session->value.is_dict());
EXPECT_EQ("App display name", CastSession::GetRouteDescription(*session));
}
TEST(CastInternalMessageUtilTest, CastSessionFromInvalidReceiverStatuses) {
MediaSinkInternal sink = CreateCastSink(1);
std::string missing_app_id = R"({
"applications": [{
"displayName": "App display name",
"namespaces": [
{"name": "urn:x-cast:com.google.cast.media"},
{"name": "urn:x-cast:com.google.foo"}
],
"sessionId": "sessionId",
"statusText":"App status",
"transportId":"transportId"
}]
})";
ExpectNoCastSession(sink, missing_app_id, "missing app id");
std::string missing_display_name = R"({
"applications": [{
"appId": "ABCDEFGH",
"namespaces": [
{"name": "urn:x-cast:com.google.cast.media"},
{"name": "urn:x-cast:com.google.foo"}
],
"sessionId": "sessionId",
"statusText":"App status",
"transportId":"transportId"
}]
})";
ExpectNoCastSession(sink, missing_display_name, "missing display name");
std::string missing_namespaces = R"({
"applications": [{
"appId": "ABCDEFGH",
"displayName": "App display name",
"namespaces": [],
"sessionId": "sessionId",
"statusText":"App status",
"transportId":"transportId"
}]
})";
ExpectNoCastSession(sink, missing_namespaces, "missing namespaces");
std::string missing_session_id = R"({
"applications": [{
"appId": "ABCDEFGH",
"displayName": "App display name",
"namespaces": [
{"name": "urn:x-cast:com.google.cast.media"},
{"name": "urn:x-cast:com.google.foo"}
],
"statusText":"App status",
"transportId":"transportId"
}]
})";
ExpectNoCastSession(sink, missing_session_id, "missing session id");
std::string missing_transport_id = R"({
"applications": [{
"appId": "ABCDEFGH",
"displayName": "App display name",
"namespaces": [
{"name": "urn:x-cast:com.google.cast.media"},
{"name": "urn:x-cast:com.google.foo"}
],
"sessionId": "sessionId",
"statusText":"App status"
}]
})";
ExpectNoCastSession(sink, missing_transport_id, "missing transport id");
}
TEST(CastInternalMessageUtilTest, CreateReceiverActionCastMessage) {
std::string client_id = "clientId";
MediaSinkInternal sink = CreateCastSink(1);
std::string expected_message = R"({
"clientId": "clientId",
"message": {
"action": "cast",
"receiver": {
"capabilities": [ "video_out", "audio_out" ],
"displayStatus": null,
"friendlyName": "friendly name 1",
"isActiveInput": null,
"label": "yYH_HCL9CKJFmvKJ9m3Une2cS8s",
"receiverType": "cast",
"volume": null
}
},
"sequenceNumber": -1,
"timeoutMillis": 0,
"type": "receiver_action"
})";
auto message =
CreateReceiverActionCastMessage(client_id, sink, kReceiverIdToken);
ExpectJSONMessagesEqual(expected_message, message->get_message());
}
TEST(CastInternalMessageUtilTest, CreateReceiverActionStopMessage) {
std::string client_id = "clientId";
MediaSinkInternal sink = CreateCastSink(1);
std::string expected_message = R"({
"clientId": "clientId",
"message": {
"action": "stop",
"receiver": {
"capabilities": [ "video_out", "audio_out" ],
"displayStatus": null,
"friendlyName": "friendly name 1",
"isActiveInput": null,
"label": "yYH_HCL9CKJFmvKJ9m3Une2cS8s",
"receiverType": "cast",
"volume": null
}
},
"sequenceNumber": -1,
"timeoutMillis": 0,
"type": "receiver_action"
})";
auto message =
CreateReceiverActionStopMessage(client_id, sink, kReceiverIdToken);
ExpectJSONMessagesEqual(expected_message, message->get_message());
}
TEST(CastInternalMessageUtilTest, CreateNewSessionMessage) {
MediaSinkInternal sink = CreateCastSink(1);
std::string client_id = "clientId";
auto receiver_status = ReceiverStatus();
ASSERT_TRUE(receiver_status);
auto session = CastSession::From(sink, kReceiverIdToken, *receiver_status);
ASSERT_TRUE(session);
std::string expected_message = R"({
"clientId": "clientId",
"message": {
"appId": "ABCDEFGH",
"appImages": [ ],
"displayName": "App display name",
"namespaces": [ {
"name": "urn:x-cast:com.google.cast.media"
}, {
"name": "urn:x-cast:com.google.foo"
} ],
"receiver": {
"capabilities": [ "video_out", "audio_out" ],
"displayStatus": null,
"friendlyName": "friendly name 1",
"isActiveInput": null,
"label": "yYH_HCL9CKJFmvKJ9m3Une2cS8s",
"receiverType": "cast",
"volume": null
},
"senderApps": [ ],
"sessionId": "sessionId",
"statusText": "App status",
"transportId": "transportId"
},
"sequenceNumber": -1,
"timeoutMillis": 0,
"type": "new_session"
})";
auto message = CreateNewSessionMessage(*session, client_id);
ExpectJSONMessagesEqual(expected_message, message->get_message());
}
TEST(CastInternalMessageUtilTest, CreateAppMessageAck) {
std::string client_id = "clientId";
int sequence_number = 12345;
std::string expected_message = R"({
"clientId": "clientId",
"message": null,
"sequenceNumber": 12345,
"timeoutMillis": 0,
"type": "app_message"
})";
auto message = CreateAppMessageAck(client_id, sequence_number);
ExpectJSONMessagesEqual(expected_message, message->get_message());
}
TEST(CastInternalMessageUtilTest, CreateAppMessage) {
std::string session_id = "sessionId";
std::string client_id = "clientId";
base::Value message_body(base::Value::Type::DICTIONARY);
message_body.SetKey("foo", base::Value("bar"));
cast_channel::CastMessage cast_message = cast_channel::CreateCastMessage(
"urn:x-cast:com.google.foo", message_body, "sourceId", "destinationId");
std::string expected_message = R"({
"clientId": "clientId",
"message": {
"message": "{\"foo\":\"bar\"}",
"namespaceName": "urn:x-cast:com.google.foo",
"sessionId": "sessionId"
},
"sequenceNumber": -1,
"timeoutMillis": 0,
"type": "app_message"
})";
auto message = CreateAppMessage(session_id, client_id, cast_message);
ExpectJSONMessagesEqual(expected_message, message->get_message());
}
} // namespace media_router
...@@ -64,7 +64,7 @@ TEST_F(CastMediaRouteProviderTest, StartObservingMediaSinks) { ...@@ -64,7 +64,7 @@ TEST_F(CastMediaRouteProviderTest, StartObservingMediaSinks) {
EXPECT_CALL(app_discovery_service_, DoStartObservingMediaSinks(_)).Times(0); EXPECT_CALL(app_discovery_service_, DoStartObservingMediaSinks(_)).Times(0);
provider_->StartObservingMediaSinks(non_cast_source); provider_->StartObservingMediaSinks(non_cast_source);
MediaSource::Id cast_source("cast:ABCDEFGH"); MediaSource::Id cast_source("cast:ABCDEFGH?clientId=123");
EXPECT_CALL(app_discovery_service_, DoStartObservingMediaSinks(_)); EXPECT_CALL(app_discovery_service_, DoStartObservingMediaSinks(_));
provider_->StartObservingMediaSinks(cast_source); provider_->StartObservingMediaSinks(cast_source);
EXPECT_FALSE(app_discovery_service_.callbacks().empty()); EXPECT_FALSE(app_discovery_service_.callbacks().empty());
...@@ -78,6 +78,7 @@ TEST_F(CastMediaRouteProviderTest, BroadcastRequest) { ...@@ -78,6 +78,7 @@ TEST_F(CastMediaRouteProviderTest, BroadcastRequest) {
media_sink_service_.AddOrUpdateSink(CreateCastSink(2)); media_sink_service_.AddOrUpdateSink(CreateCastSink(2));
MediaSource::Id source_id( MediaSource::Id source_id(
"cast:ABCDEFAB?capabilities=video_out,audio_out" "cast:ABCDEFAB?capabilities=video_out,audio_out"
"&clientId=123"
"&broadcastNamespace=namespace" "&broadcastNamespace=namespace"
"&broadcastMessage=message"); "&broadcastMessage=message");
......
...@@ -28,6 +28,7 @@ ...@@ -28,6 +28,7 @@
#include "chrome/browser/media/media_device_id_salt.h" #include "chrome/browser/media/media_device_id_salt.h"
#include "chrome/browser/media/media_engagement_service.h" #include "chrome/browser/media/media_engagement_service.h"
#include "chrome/browser/media/media_storage_id_salt.h" #include "chrome/browser/media/media_storage_id_salt.h"
#include "chrome/browser/media/router/media_router_feature.h"
#include "chrome/browser/media/webrtc/media_capture_devices_dispatcher.h" #include "chrome/browser/media/webrtc/media_capture_devices_dispatcher.h"
#include "chrome/browser/media/webrtc/media_stream_devices_controller.h" #include "chrome/browser/media/webrtc/media_stream_devices_controller.h"
#include "chrome/browser/metrics/chrome_metrics_service_client.h" #include "chrome/browser/metrics/chrome_metrics_service_client.h"
...@@ -651,6 +652,7 @@ void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry) { ...@@ -651,6 +652,7 @@ void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry) {
InstantService::RegisterProfilePrefs(registry); InstantService::RegisterProfilePrefs(registry);
gcm::GCMChannelStatusSyncer::RegisterProfilePrefs(registry); gcm::GCMChannelStatusSyncer::RegisterProfilePrefs(registry);
gcm::RegisterProfilePrefs(registry); gcm::RegisterProfilePrefs(registry);
media_router::RegisterProfilePrefs(registry);
ntp_tiles::CustomLinksManagerImpl::RegisterProfilePrefs(registry); ntp_tiles::CustomLinksManagerImpl::RegisterProfilePrefs(registry);
StartupBrowserCreator::RegisterProfilePrefs(registry); StartupBrowserCreator::RegisterProfilePrefs(registry);
#endif #endif
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
#include "chrome/common/media_router/providers/cast/cast_media_source.h" #include "chrome/common/media_router/providers/cast/cast_media_source.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h" #include "base/strings/string_split.h"
#include "base/strings/utf_string_conversions.h" #include "base/strings/utf_string_conversions.h"
#include "build/build_config.h" #include "build/build_config.h"
...@@ -18,18 +19,19 @@ namespace media_router { ...@@ -18,18 +19,19 @@ namespace media_router {
namespace { namespace {
constexpr char kMirroringAppId[] = "0F5096E8";
constexpr char kAudioMirroringAppId[] = "85CDB22F";
// Parameter keys used by new Cast URLs. // Parameter keys used by new Cast URLs.
constexpr char kCapabilitiesKey[] = "capabilities"; constexpr char kCapabilitiesKey[] = "capabilities";
constexpr char kBroadcastNamespaceKey[] = "broadcastNamespace"; constexpr char kBroadcastNamespaceKey[] = "broadcastNamespace";
constexpr char kBroadcastMessageKey[] = "broadcastMessage"; constexpr char kBroadcastMessageKey[] = "broadcastMessage";
constexpr char kClientIdKey[] = "clientId";
constexpr char kLaunchTimeoutKey[] = "launchTimeout";
// Parameter keys used by legacy Cast URLs. // Parameter keys used by legacy Cast URLs.
constexpr char kLegacyAppIdKey[] = "__castAppId__"; constexpr char kLegacyAppIdKey[] = "__castAppId__";
constexpr char kLegacyBroadcastNamespaceKey[] = "__castBroadcastNamespace__"; constexpr char kLegacyBroadcastNamespaceKey[] = "__castBroadcastNamespace__";
constexpr char kLegacyBroadcastMessageKey[] = "__castBroadcastMessage__"; constexpr char kLegacyBroadcastMessageKey[] = "__castBroadcastMessage__";
constexpr char kLegacyClientIdKey[] = "__castClientId__";
constexpr char kLegacyLaunchTimeoutKey[] = "__castLaunchTimeout__";
// TODO(imcheng): Move to common utils? // TODO(imcheng): Move to common utils?
std::string DecodeURLComponent(const std::string& encoded) { std::string DecodeURLComponent(const std::string& encoded) {
...@@ -61,8 +63,9 @@ cast_channel::CastDeviceCapability CastDeviceCapabilityFromString( ...@@ -61,8 +63,9 @@ cast_channel::CastDeviceCapability CastDeviceCapabilityFromString(
std::unique_ptr<CastMediaSource> CastMediaSourceForTabMirroring( std::unique_ptr<CastMediaSource> CastMediaSourceForTabMirroring(
const MediaSource::Id& source_id) { const MediaSource::Id& source_id) {
return std::make_unique<CastMediaSource>( return std::make_unique<CastMediaSource>(
source_id, std::vector<CastAppInfo>({CastAppInfo(kMirroringAppId), source_id,
CastAppInfo(kAudioMirroringAppId)})); std::vector<CastAppInfo>({CastAppInfo(kCastStreamingAppId),
CastAppInfo(kCastStreamingAudioAppId)}));
} }
std::unique_ptr<CastMediaSource> CastMediaSourceForDesktopMirroring( std::unique_ptr<CastMediaSource> CastMediaSourceForDesktopMirroring(
...@@ -70,28 +73,33 @@ std::unique_ptr<CastMediaSource> CastMediaSourceForDesktopMirroring( ...@@ -70,28 +73,33 @@ std::unique_ptr<CastMediaSource> CastMediaSourceForDesktopMirroring(
// Desktop audio mirroring is only supported on some platforms. // Desktop audio mirroring is only supported on some platforms.
#if defined(OS_WIN) || defined(OS_CHROMEOS) #if defined(OS_WIN) || defined(OS_CHROMEOS)
return std::make_unique<CastMediaSource>( return std::make_unique<CastMediaSource>(
source_id, std::vector<CastAppInfo>({CastAppInfo(kMirroringAppId), source_id,
CastAppInfo(kAudioMirroringAppId)})); std::vector<CastAppInfo>({CastAppInfo(kCastStreamingAppId),
CastAppInfo(kCastStreamingAudioAppId)}));
#else #else
return std::make_unique<CastMediaSource>( return std::make_unique<CastMediaSource>(
source_id, std::vector<CastAppInfo>({CastAppInfo(kMirroringAppId)})); source_id, std::vector<CastAppInfo>({CastAppInfo(kCastStreamingAppId)}));
#endif #endif
} }
std::unique_ptr<CastMediaSource> CreateFromURLParams( std::unique_ptr<CastMediaSource> CreateFromURLParams(
const MediaSource::Id& source_id, const MediaSource::Id& source_id,
const std::vector<CastAppInfo>& app_infos, const std::vector<CastAppInfo>& app_infos,
const std::string& client_id,
const std::string& broadcast_namespace, const std::string& broadcast_namespace,
const std::string& broadcast_message) { const std::string& broadcast_message,
base::TimeDelta launch_timeout) {
if (app_infos.empty()) if (app_infos.empty())
return nullptr; return nullptr;
auto cast_source = std::make_unique<CastMediaSource>(source_id, app_infos); auto cast_source = std::make_unique<CastMediaSource>(source_id, app_infos);
cast_source->set_client_id(client_id);
if (!broadcast_namespace.empty() && !broadcast_message.empty()) { if (!broadcast_namespace.empty() && !broadcast_message.empty()) {
cast_source->set_broadcast_request( cast_source->set_broadcast_request(
BroadcastRequest(broadcast_namespace, broadcast_message)); BroadcastRequest(broadcast_namespace, broadcast_message));
} }
if (launch_timeout > base::TimeDelta())
cast_source->set_launch_timeout(launch_timeout);
return cast_source; return cast_source;
} }
...@@ -103,6 +111,8 @@ std::unique_ptr<CastMediaSource> ParseCastUrl(const MediaSource::Id& source_id, ...@@ -103,6 +111,8 @@ std::unique_ptr<CastMediaSource> ParseCastUrl(const MediaSource::Id& source_id,
return nullptr; return nullptr;
std::string broadcast_namespace, broadcast_message, capabilities; std::string broadcast_namespace, broadcast_message, capabilities;
std::string client_id;
int launch_timeout_millis = 0;
for (net::QueryIterator query_it(url); !query_it.IsAtEnd(); for (net::QueryIterator query_it(url); !query_it.IsAtEnd();
query_it.Advance()) { query_it.Advance()) {
std::string key = query_it.GetKey(); std::string key = query_it.GetKey();
...@@ -116,6 +126,12 @@ std::unique_ptr<CastMediaSource> ParseCastUrl(const MediaSource::Id& source_id, ...@@ -116,6 +126,12 @@ std::unique_ptr<CastMediaSource> ParseCastUrl(const MediaSource::Id& source_id,
broadcast_message = DecodeURLComponent(value); broadcast_message = DecodeURLComponent(value);
} else if (key == kCapabilitiesKey) { } else if (key == kCapabilitiesKey) {
capabilities = value; capabilities = value;
} else if (key == kClientIdKey) {
client_id = value;
} else if (key == kLaunchTimeoutKey) {
if (!base::StringToInt(value, &launch_timeout_millis) ||
launch_timeout_millis < 0)
launch_timeout_millis = 0;
} }
} }
...@@ -129,8 +145,9 @@ std::unique_ptr<CastMediaSource> ParseCastUrl(const MediaSource::Id& source_id, ...@@ -129,8 +145,9 @@ std::unique_ptr<CastMediaSource> ParseCastUrl(const MediaSource::Id& source_id,
} }
} }
return CreateFromURLParams(source_id, {app_info}, broadcast_namespace, return CreateFromURLParams(
broadcast_message); source_id, {app_info}, client_id, broadcast_namespace, broadcast_message,
base::TimeDelta::FromMilliseconds(launch_timeout_millis));
} }
std::unique_ptr<CastMediaSource> ParseLegacyCastUrl( std::unique_ptr<CastMediaSource> ParseLegacyCastUrl(
...@@ -141,6 +158,8 @@ std::unique_ptr<CastMediaSource> ParseLegacyCastUrl( ...@@ -141,6 +158,8 @@ std::unique_ptr<CastMediaSource> ParseLegacyCastUrl(
// Legacy URLs can specify multiple apps. // Legacy URLs can specify multiple apps.
std::vector<std::string> app_id_params; std::vector<std::string> app_id_params;
std::string broadcast_namespace, broadcast_message; std::string broadcast_namespace, broadcast_message;
std::string client_id;
int launch_timeout_millis = 0;
for (const auto& key_value : parameters) { for (const auto& key_value : parameters) {
const auto& key = key_value.first; const auto& key = key_value.first;
const auto& value = key_value.second; const auto& value = key_value.second;
...@@ -151,6 +170,12 @@ std::unique_ptr<CastMediaSource> ParseLegacyCastUrl( ...@@ -151,6 +170,12 @@ std::unique_ptr<CastMediaSource> ParseLegacyCastUrl(
} else if (key == kLegacyBroadcastMessageKey) { } else if (key == kLegacyBroadcastMessageKey) {
// The broadcast message is URL-encoded. // The broadcast message is URL-encoded.
broadcast_message = DecodeURLComponent(value); broadcast_message = DecodeURLComponent(value);
} else if (key == kLegacyClientIdKey) {
client_id = value;
} else if (key == kLegacyLaunchTimeoutKey) {
if (!base::StringToInt(value, &launch_timeout_millis) ||
launch_timeout_millis < 0)
launch_timeout_millis = 0;
} }
} }
...@@ -187,8 +212,9 @@ std::unique_ptr<CastMediaSource> ParseLegacyCastUrl( ...@@ -187,8 +212,9 @@ std::unique_ptr<CastMediaSource> ParseLegacyCastUrl(
if (app_infos.empty()) if (app_infos.empty())
return nullptr; return nullptr;
return CreateFromURLParams(source_id, app_infos, broadcast_namespace, return CreateFromURLParams(
broadcast_message); source_id, app_infos, client_id, broadcast_namespace, broadcast_message,
base::TimeDelta::FromMilliseconds(launch_timeout_millis));
} }
} // namespace } // namespace
...@@ -225,9 +251,6 @@ std::unique_ptr<CastMediaSource> CastMediaSource::From( ...@@ -225,9 +251,6 @@ std::unique_ptr<CastMediaSource> CastMediaSource::From(
return nullptr; return nullptr;
} }
CastMediaSource::CastMediaSource(const MediaSource::Id& source_id,
const CastAppInfo& app_info)
: source_id_(source_id), app_infos_({app_info}) {}
CastMediaSource::CastMediaSource(const MediaSource::Id& source_id, CastMediaSource::CastMediaSource(const MediaSource::Id& source_id,
const std::vector<CastAppInfo>& app_infos) const std::vector<CastAppInfo>& app_infos)
: source_id_(source_id), app_infos_(app_infos) {} : source_id_(source_id), app_infos_(app_infos) {}
......
...@@ -16,6 +16,16 @@ ...@@ -16,6 +16,16 @@
namespace media_router { namespace media_router {
static constexpr char kCastStreamingAppId[] = "0F5096E8";
static constexpr char kCastStreamingAudioAppId[] = "85CDB22F";
// Placeholder app ID advertised by the multizone leader in a receiver status
// message.
static constexpr char kMultizoneLeaderAppId[] = "MultizoneLeader";
static constexpr base::TimeDelta kDefaultLaunchTimeout =
base::TimeDelta::FromSeconds(60);
// Represents a Cast app and its capabilitity requirements. // Represents a Cast app and its capabilitity requirements.
struct CastAppInfo { struct CastAppInfo {
explicit CastAppInfo(const std::string& app_id); explicit CastAppInfo(const std::string& app_id);
...@@ -39,10 +49,8 @@ class CastMediaSource { ...@@ -39,10 +49,8 @@ class CastMediaSource {
// Returns the parsed form of |source|, or nullptr if it cannot be parsed. // Returns the parsed form of |source|, or nullptr if it cannot be parsed.
static std::unique_ptr<CastMediaSource> From(const MediaSource::Id& source); static std::unique_ptr<CastMediaSource> From(const MediaSource::Id& source);
explicit CastMediaSource(const MediaSource::Id& source_id, CastMediaSource(const MediaSource::Id& source_id,
const CastAppInfo& app_info); const std::vector<CastAppInfo>& app_infos);
explicit CastMediaSource(const MediaSource::Id& source_id,
const std::vector<CastAppInfo>& app_infos);
CastMediaSource(const CastMediaSource& other); CastMediaSource(const CastMediaSource& other);
~CastMediaSource(); ~CastMediaSource();
...@@ -55,6 +63,12 @@ class CastMediaSource { ...@@ -55,6 +63,12 @@ class CastMediaSource {
const MediaSource::Id& source_id() const { return source_id_; } const MediaSource::Id& source_id() const { return source_id_; }
const std::vector<CastAppInfo>& app_infos() const { return app_infos_; } const std::vector<CastAppInfo>& app_infos() const { return app_infos_; }
const std::string& client_id() const { return client_id_; }
void set_client_id(const std::string& client_id) { client_id_ = client_id; }
base::TimeDelta launch_timeout() const { return launch_timeout_; }
void set_launch_timeout(base::TimeDelta launch_timeout) {
launch_timeout_ = launch_timeout;
}
const base::Optional<cast_channel::BroadcastRequest>& broadcast_request() const base::Optional<cast_channel::BroadcastRequest>& broadcast_request()
const { const {
return broadcast_request_; return broadcast_request_;
...@@ -68,6 +82,9 @@ class CastMediaSource { ...@@ -68,6 +82,9 @@ class CastMediaSource {
// TODO(imcheng): Fill in other parameters. // TODO(imcheng): Fill in other parameters.
MediaSource::Id source_id_; MediaSource::Id source_id_;
std::vector<CastAppInfo> app_infos_; std::vector<CastAppInfo> app_infos_;
base::TimeDelta launch_timeout_ = kDefaultLaunchTimeout;
// Empty if not set.
std::string client_id_;
base::Optional<cast_channel::BroadcastRequest> broadcast_request_; base::Optional<cast_channel::BroadcastRequest> broadcast_request_;
}; };
......
...@@ -13,7 +13,9 @@ TEST(CastMediaSourceTest, FromCastURL) { ...@@ -13,7 +13,9 @@ TEST(CastMediaSourceTest, FromCastURL) {
MediaSource::Id source_id( MediaSource::Id source_id(
"cast:ABCDEFAB?capabilities=video_out,audio_out" "cast:ABCDEFAB?capabilities=video_out,audio_out"
"&broadcastNamespace=namespace" "&broadcastNamespace=namespace"
"&broadcastMessage=message"); "&broadcastMessage=message"
"&clientId=12345"
"&launchTimeout=30000");
std::unique_ptr<CastMediaSource> source = CastMediaSource::From(source_id); std::unique_ptr<CastMediaSource> source = CastMediaSource::From(source_id);
ASSERT_TRUE(source); ASSERT_TRUE(source);
EXPECT_EQ(source_id, source->source_id()); EXPECT_EQ(source_id, source->source_id());
...@@ -27,13 +29,17 @@ TEST(CastMediaSourceTest, FromCastURL) { ...@@ -27,13 +29,17 @@ TEST(CastMediaSourceTest, FromCastURL) {
ASSERT_TRUE(broadcast_request); ASSERT_TRUE(broadcast_request);
EXPECT_EQ("namespace", broadcast_request->broadcast_namespace); EXPECT_EQ("namespace", broadcast_request->broadcast_namespace);
EXPECT_EQ("message", broadcast_request->message); EXPECT_EQ("message", broadcast_request->message);
EXPECT_EQ("12345", source->client_id());
EXPECT_EQ(base::TimeDelta::FromMilliseconds(30000), source->launch_timeout());
} }
TEST(CastMediaSourceTest, FromLegacyCastURL) { TEST(CastMediaSourceTest, FromLegacyCastURL) {
MediaSource::Id source_id( MediaSource::Id source_id(
"https://google.com/cast#__castAppId__=ABCDEFAB(video_out,audio_out)" "https://google.com/cast#__castAppId__=ABCDEFAB(video_out,audio_out)"
"/__castBroadcastNamespace__=namespace" "/__castBroadcastNamespace__=namespace"
"/__castBroadcastMessage__=message"); "/__castBroadcastMessage__=message"
"/__castClientId__=12345"
"/__castLaunchTimeout__=30000");
std::unique_ptr<CastMediaSource> source = CastMediaSource::From(source_id); std::unique_ptr<CastMediaSource> source = CastMediaSource::From(source_id);
ASSERT_TRUE(source); ASSERT_TRUE(source);
EXPECT_EQ(source_id, source->source_id()); EXPECT_EQ(source_id, source->source_id());
...@@ -47,6 +53,8 @@ TEST(CastMediaSourceTest, FromLegacyCastURL) { ...@@ -47,6 +53,8 @@ TEST(CastMediaSourceTest, FromLegacyCastURL) {
ASSERT_TRUE(broadcast_request); ASSERT_TRUE(broadcast_request);
EXPECT_EQ("namespace", broadcast_request->broadcast_namespace); EXPECT_EQ("namespace", broadcast_request->broadcast_namespace);
EXPECT_EQ("message", broadcast_request->message); EXPECT_EQ("message", broadcast_request->message);
EXPECT_EQ("12345", source->client_id());
EXPECT_EQ(base::TimeDelta::FromMilliseconds(30000), source->launch_timeout());
} }
TEST(CastMediaSourceTest, FromPresentationURL) { TEST(CastMediaSourceTest, FromPresentationURL) {
...@@ -57,6 +65,8 @@ TEST(CastMediaSourceTest, FromPresentationURL) { ...@@ -57,6 +65,8 @@ TEST(CastMediaSourceTest, FromPresentationURL) {
ASSERT_EQ(2u, source->app_infos().size()); ASSERT_EQ(2u, source->app_infos().size());
EXPECT_EQ("0F5096E8", source->app_infos()[0].app_id); EXPECT_EQ("0F5096E8", source->app_infos()[0].app_id);
EXPECT_EQ("85CDB22F", source->app_infos()[1].app_id); EXPECT_EQ("85CDB22F", source->app_infos()[1].app_id);
EXPECT_TRUE(source->client_id().empty());
EXPECT_EQ(kDefaultLaunchTimeout, source->launch_timeout());
} }
TEST(CastMediaSourceTest, FromMirroringURN) { TEST(CastMediaSourceTest, FromMirroringURN) {
...@@ -67,6 +77,8 @@ TEST(CastMediaSourceTest, FromMirroringURN) { ...@@ -67,6 +77,8 @@ TEST(CastMediaSourceTest, FromMirroringURN) {
ASSERT_EQ(2u, source->app_infos().size()); ASSERT_EQ(2u, source->app_infos().size());
EXPECT_EQ("0F5096E8", source->app_infos()[0].app_id); EXPECT_EQ("0F5096E8", source->app_infos()[0].app_id);
EXPECT_EQ("85CDB22F", source->app_infos()[1].app_id); EXPECT_EQ("85CDB22F", source->app_infos()[1].app_id);
EXPECT_TRUE(source->client_id().empty());
EXPECT_EQ(kDefaultLaunchTimeout, source->launch_timeout());
} }
TEST(CastMediaSourceTest, FromInvalidSource) { TEST(CastMediaSourceTest, FromInvalidSource) {
......
...@@ -3201,6 +3201,7 @@ test("unit_tests") { ...@@ -3201,6 +3201,7 @@ test("unit_tests") {
"../browser/media/router/mojo/media_sink_service_status_unittest.cc", "../browser/media/router/mojo/media_sink_service_status_unittest.cc",
"../browser/media/router/providers/cast/cast_app_availability_tracker_unittest.cc", "../browser/media/router/providers/cast/cast_app_availability_tracker_unittest.cc",
"../browser/media/router/providers/cast/cast_app_discovery_service_unittest.cc", "../browser/media/router/providers/cast/cast_app_discovery_service_unittest.cc",
"../browser/media/router/providers/cast/cast_internal_message_util_unittest.cc",
"../browser/media/router/providers/cast/cast_media_route_provider_metrics_unittest.cc", "../browser/media/router/providers/cast/cast_media_route_provider_metrics_unittest.cc",
"../browser/media/router/providers/cast/cast_media_route_provider_unittest.cc", "../browser/media/router/providers/cast/cast_media_route_provider_unittest.cc",
"../browser/media/router/providers/cast/dual_media_sink_service_unittest.cc", "../browser/media/router/providers/cast/dual_media_sink_service_unittest.cc",
......
...@@ -34,13 +34,14 @@ constexpr char kBroadcastNamespace[] = "urn:x-cast:com.google.cast.broadcast"; ...@@ -34,13 +34,14 @@ constexpr char kBroadcastNamespace[] = "urn:x-cast:com.google.cast.broadcast";
constexpr char kTypeNodeId[] = "type"; constexpr char kTypeNodeId[] = "type";
constexpr char kRequestIdNodeId[] = "requestId"; constexpr char kRequestIdNodeId[] = "requestId";
// Cast application protocol message types. // Cast application protocol message types. Keep in sync with CastMessageType.
constexpr char kKeepAlivePingType[] = "PING"; constexpr char kKeepAlivePingType[] = "PING";
constexpr char kKeepAlivePongType[] = "PONG"; constexpr char kKeepAlivePongType[] = "PONG";
constexpr char kGetAppAvailabilityRequestType[] = "GET_APP_AVAILABILITY"; constexpr char kGetAppAvailabilityRequestType[] = "GET_APP_AVAILABILITY";
constexpr char kConnectionRequestType[] = "CONNECT"; constexpr char kConnectionRequestType[] = "CONNECT";
constexpr char kBroadcastRequestType[] = "APPLICATION_BROADCAST"; constexpr char kBroadcastRequestType[] = "APPLICATION_BROADCAST";
constexpr char kLaunchRequestType[] = "LAUNCH"; constexpr char kLaunchRequestType[] = "LAUNCH";
constexpr char kStopRequestType[] = "STOP";
constexpr char kReceiverStatusType[] = "RECEIVER_STATUS"; constexpr char kReceiverStatusType[] = "RECEIVER_STATUS";
constexpr char kLaunchErrorType[] = "LAUNCH_ERROR"; constexpr char kLaunchErrorType[] = "LAUNCH_ERROR";
...@@ -107,6 +108,7 @@ std::unique_ptr<base::DictionaryValue> GetDictionaryFromCastMessage( ...@@ -107,6 +108,7 @@ std::unique_ptr<base::DictionaryValue> GetDictionaryFromCastMessage(
if (!message.has_payload_utf8()) if (!message.has_payload_utf8())
return nullptr; return nullptr;
// TODO(https://crbug.com/809249): Parse JSON using data_decoder service.
return base::DictionaryValue::From( return base::DictionaryValue::From(
base::JSONReader::Read(message.payload_utf8())); base::JSONReader::Read(message.payload_utf8()));
} }
...@@ -146,6 +148,8 @@ const char* CastMessageTypeToString(CastMessageType message_type) { ...@@ -146,6 +148,8 @@ const char* CastMessageTypeToString(CastMessageType message_type) {
return kBroadcastRequestType; return kBroadcastRequestType;
case CastMessageType::kLaunch: case CastMessageType::kLaunch:
return kLaunchRequestType; return kLaunchRequestType;
case CastMessageType::kStop:
return kStopRequestType;
case CastMessageType::kReceiverStatus: case CastMessageType::kReceiverStatus:
return kReceiverStatusType; return kReceiverStatusType;
case CastMessageType::kLaunchError: case CastMessageType::kLaunchError:
...@@ -170,6 +174,8 @@ CastMessageType CastMessageTypeFromString(const std::string& type) { ...@@ -170,6 +174,8 @@ CastMessageType CastMessageTypeFromString(const std::string& type) {
return CastMessageType::kBroadcast; return CastMessageType::kBroadcast;
if (type == kLaunchRequestType) if (type == kLaunchRequestType)
return CastMessageType::kLaunch; return CastMessageType::kLaunch;
if (type == kStopRequestType)
return CastMessageType::kStop;
if (type == kReceiverStatusType) if (type == kReceiverStatusType)
return CastMessageType::kReceiverStatus; return CastMessageType::kReceiverStatus;
if (type == kLaunchErrorType) if (type == kLaunchErrorType)
...@@ -347,6 +353,17 @@ CastMessage CreateLaunchRequest(const std::string& source_id, ...@@ -347,6 +353,17 @@ CastMessage CreateLaunchRequest(const std::string& source_id,
kPlatformReceiverId); kPlatformReceiverId);
} }
CastMessage CreateStopRequest(const std::string& source_id,
int request_id,
const std::string& session_id) {
Value dict(Value::Type::DICTIONARY);
dict.SetKey(kTypeNodeId, Value(kStopRequestType));
dict.SetKey(kRequestIdNodeId, Value(request_id));
dict.SetKey("sessionId", Value(session_id));
return CreateCastMessage(kReceiverNamespace, dict, source_id,
kPlatformReceiverId);
}
CastMessage CreateCastMessage(const std::string& message_namespace, CastMessage CreateCastMessage(const std::string& message_namespace,
const base::Value& message, const base::Value& message,
const std::string& source_id, const std::string& source_id,
...@@ -406,8 +423,7 @@ LaunchSessionResponse::LaunchSessionResponse(LaunchSessionResponse&& other) = ...@@ -406,8 +423,7 @@ LaunchSessionResponse::LaunchSessionResponse(LaunchSessionResponse&& other) =
default; default;
LaunchSessionResponse::~LaunchSessionResponse() = default; LaunchSessionResponse::~LaunchSessionResponse() = default;
LaunchSessionResponse GetLaunchSessionResponse( LaunchSessionResponse GetLaunchSessionResponse(const base::Value& payload) {
const base::DictionaryValue& payload) {
const Value* type_value = const Value* type_value =
payload.FindKeyOfType(kTypeNodeId, Value::Type::STRING); payload.FindKeyOfType(kTypeNodeId, Value::Type::STRING);
if (!type_value) if (!type_value)
......
...@@ -27,6 +27,7 @@ enum class CastMessageType { ...@@ -27,6 +27,7 @@ enum class CastMessageType {
kConnect, // Virtual connection request kConnect, // Virtual connection request
kBroadcast, // Application broadcast / precache kBroadcast, // Application broadcast / precache
kLaunch, // Session launch request kLaunch, // Session launch request
kStop, // Session stop request
kReceiverStatus, kReceiverStatus,
kLaunchError, kLaunchError,
kOther // Add new types above |kOther|. kOther // Add new types above |kOther|.
...@@ -132,6 +133,10 @@ CastMessage CreateLaunchRequest(const std::string& source_id, ...@@ -132,6 +133,10 @@ CastMessage CreateLaunchRequest(const std::string& source_id,
const std::string& app_id, const std::string& app_id,
const std::string& locale); const std::string& locale);
CastMessage CreateStopRequest(const std::string& source_id,
int request_id,
const std::string& session_id);
// Creates a generic CastMessage with |message| as the string payload. Used for // Creates a generic CastMessage with |message| as the string payload. Used for
// app messages. // app messages.
CastMessage CreateCastMessage(const std::string& message_namespace, CastMessage CreateCastMessage(const std::string& message_namespace,
...@@ -168,16 +173,15 @@ struct LaunchSessionResponse { ...@@ -168,16 +173,15 @@ struct LaunchSessionResponse {
~LaunchSessionResponse(); ~LaunchSessionResponse();
Result result = Result::kUnknown; Result result = Result::kUnknown;
// Populated if |result| is |kOk|. // Populated if |result| is |kOk|.
base::Optional<base::Value> receiver_status; base::Optional<base::Value> receiver_status;
}; };
// Parses |payload| into a LaunchSessionResponse. Returns an empty // Parses |payload| into a LaunchSessionResponse. Returns an empty
// LaunchSessionResponse if |payload| is not a properly formatted launch // LaunchSessionResponse if |payload| is not a properly formatted launch
// response. |payload| must be from the string payload of a CastMessage. // response. |payload| must be a dictionary from the string payload of a
LaunchSessionResponse GetLaunchSessionResponse( // CastMessage.
const base::DictionaryValue& payload); LaunchSessionResponse GetLaunchSessionResponse(const base::Value& payload);
} // namespace cast_channel } // namespace cast_channel
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
#include "base/json/json_reader.h" #include "base/json/json_reader.h"
#include "base/values.h" #include "base/values.h"
#include "components/cast_channel/proto/cast_channel.pb.h"
#include "testing/gtest/include/gtest/gtest.h" #include "testing/gtest/include/gtest/gtest.h"
namespace cast_channel { namespace cast_channel {
...@@ -34,8 +35,7 @@ TEST(CastMessageUtilTest, GetLaunchSessionResponseOk) { ...@@ -34,8 +35,7 @@ TEST(CastMessageUtilTest, GetLaunchSessionResponseOk) {
} }
)"; )";
std::unique_ptr<base::DictionaryValue> value = std::unique_ptr<base::Value> value = base::JSONReader::Read(payload);
base::DictionaryValue::From(base::JSONReader::Read(payload));
ASSERT_TRUE(value); ASSERT_TRUE(value);
LaunchSessionResponse response = GetLaunchSessionResponse(*value); LaunchSessionResponse response = GetLaunchSessionResponse(*value);
...@@ -51,8 +51,7 @@ TEST(CastMessageUtilTest, GetLaunchSessionResponseError) { ...@@ -51,8 +51,7 @@ TEST(CastMessageUtilTest, GetLaunchSessionResponseError) {
} }
)"; )";
std::unique_ptr<base::DictionaryValue> value = std::unique_ptr<base::Value> value = base::JSONReader::Read(payload);
base::DictionaryValue::From(base::JSONReader::Read(payload));
ASSERT_TRUE(value); ASSERT_TRUE(value);
LaunchSessionResponse response = GetLaunchSessionResponse(*value); LaunchSessionResponse response = GetLaunchSessionResponse(*value);
...@@ -70,8 +69,7 @@ TEST(CastMessageUtilTest, GetLaunchSessionResponseUnknown) { ...@@ -70,8 +69,7 @@ TEST(CastMessageUtilTest, GetLaunchSessionResponseUnknown) {
} }
)"; )";
std::unique_ptr<base::DictionaryValue> value = std::unique_ptr<base::Value> value = base::JSONReader::Read(payload);
base::DictionaryValue::From(base::JSONReader::Read(payload));
ASSERT_TRUE(value); ASSERT_TRUE(value);
LaunchSessionResponse response = GetLaunchSessionResponse(*value); LaunchSessionResponse response = GetLaunchSessionResponse(*value);
...@@ -79,4 +77,26 @@ TEST(CastMessageUtilTest, GetLaunchSessionResponseUnknown) { ...@@ -79,4 +77,26 @@ TEST(CastMessageUtilTest, GetLaunchSessionResponseUnknown) {
EXPECT_FALSE(response.receiver_status); EXPECT_FALSE(response.receiver_status);
} }
TEST(CastMessageUtilTest, CreateStopRequest) {
std::string expected_message = R"(
{
"type": "STOP",
"requestId": 123,
"sessionId": "sessionId"
}
)";
std::unique_ptr<base::Value> expected_value =
base::JSONReader::Read(expected_message);
ASSERT_TRUE(expected_value);
CastMessage message = CreateStopRequest("sourceId", 123, "sessionId");
ASSERT_TRUE(IsCastMessageValid(message));
std::unique_ptr<base::Value> actual_value =
base::JSONReader::Read(message.payload_utf8());
ASSERT_TRUE(actual_value);
EXPECT_EQ(*expected_value, *actual_value);
}
} // namespace cast_channel } // namespace cast_channel
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment