blob: 2fd7da570d86b610416e3f2a76838acf834ceeda [file] [log] [blame]
// 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 {
// The ID for the backdrop app. Cast devices running the backdrop app is
// considered idle, and an active session should not be reported.
constexpr char kBackdropAppId[] = "E8C28D3C";
constexpr char kClientConnect[] = "client_connect";
constexpr char kAppMessage[] = "app_message";
constexpr char kV2Message[] = "v2_message";
constexpr char kReceiverAction[] = "receiver_action";
constexpr char kNewSession[] = "new_session";
constexpr char kUpdateSession[] = "update_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 == kV2Message)
return CastInternalMessage::Type::kV2Message;
if (type == kReceiverAction)
return CastInternalMessage::Type::kReceiverAction;
if (type == kNewSession)
return CastInternalMessage::Type::kNewSession;
if (type == kUpdateSession)
return CastInternalMessage::Type::kUpdateSession;
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::kV2Message:
return kV2Message;
case CastInternalMessage::Type::kReceiverAction:
return kReceiverAction;
case CastInternalMessage::Type::kNewSession:
return kNewSession;
case CastInternalMessage::Type::kUpdateSession:
return kUpdateSession;
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;
}
std::string GetReceiverLabel(const MediaSinkInternal& sink,
const std::string& hash_token) {
std::string label = base::SHA1HashString(sink.sink().id() + hash_token);
base::Base64UrlEncode(label, base::Base64UrlEncodePolicy::OMIT_PADDING,
&label);
return label;
}
base::Value CreateReceiver(const MediaSinkInternal& sink,
const std::string& hash_token) {
base::Value receiver(base::Value::Type::DICTIONARY);
if (!hash_token.empty()) {
receiver.SetKey("label", base::Value(GetReceiverLabel(sink, hash_token)));
}
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;
}
blink::mojom::PresentationConnectionMessagePtr CreateMessageCommon(
CastInternalMessage::Type type,
base::Value payload,
const std::string& client_id,
base::Optional<int> sequence_number = base::nullopt) {
base::Value message(base::Value::Type::DICTIONARY);
message.SetKey("type", base::Value(CastInternalMessageTypeToString(type)));
message.SetKey("message", std::move(payload));
if (sequence_number) {
message.SetKey("sequenceNumber", base::Value(*sequence_number));
}
message.SetKey("timeoutMillis", base::Value(0));
message.SetKey("clientId", base::Value(client_id));
std::string str;
CHECK(base::JSONWriter::Write(message, &str));
return blink::mojom::PresentationConnectionMessage::NewMessage(str);
}
blink::mojom::PresentationConnectionMessagePtr 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));
return CreateMessageCommon(CastInternalMessage::Type::kReceiverAction,
std::move(message), client_id);
}
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;
}
// Creates a message with a session value in the "message" field. |type| must
// be either kNewSession or kUpdateSession.
blink::mojom::PresentationConnectionMessagePtr CreateSessionMessage(
const CastSession& session,
const std::string& client_id,
const MediaSinkInternal& sink,
const std::string& hash_token,
CastInternalMessage::Type type) {
DCHECK(type == CastInternalMessage::Type::kNewSession ||
type == CastInternalMessage::Type::kUpdateSession);
base::Value session_with_receiver_label = session.value().Clone();
DCHECK(!session_with_receiver_label.FindPath({"receiver", "label"}));
session_with_receiver_label.SetPath(
{"receiver", "label"}, base::Value(GetReceiverLabel(sink, hash_token)));
return CreateMessageCommon(type, std::move(session_with_receiver_label),
client_id);
}
} // 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;
}
base::Value* sequence_number_value =
message.FindKeyOfType("sequenceNumber", base::Value::Type::INTEGER);
std::string session_id;
std::string namespace_or_v2_type;
base::Value new_message_body;
if (message_type == Type::kAppMessage || message_type == Type::kV2Message) {
if (!GetString(*message_body_value, "sessionId", &session_id)) {
DVLOG(2) << "Missing sessionId, message: " << message;
return nullptr;
}
if (!message_body_value->is_dict()) {
DVLOG(2) << "Message body is not a dict: " << message;
return nullptr;
}
if (message_type == Type::kAppMessage) {
if (!GetString(*message_body_value, "namespaceName",
&namespace_or_v2_type)) {
DVLOG(2) << "Missing namespace, 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;
}
new_message_body = std::move(*app_message_value);
} else if (message_type == CastInternalMessage::Type::kV2Message) {
if (!GetString(*message_body_value, "type", &namespace_or_v2_type)) {
DVLOG(2) << "Missing v2 type, message: " << message;
return nullptr;
}
new_message_body = std::move(*message_body_value);
}
}
return base::WrapUnique(new CastInternalMessage(
message_type, client_id,
sequence_number_value ? sequence_number_value->GetInt()
: base::Optional<int>(),
session_id, namespace_or_v2_type, std::move(new_message_body)));
}
CastInternalMessage::~CastInternalMessage() = default;
CastInternalMessage::CastInternalMessage(
Type type,
const std::string& client_id,
base::Optional<int> sequence_number,
const std::string& session_id,
const std::string& namespace_or_v2_type,
base::Value message_body)
: type(type),
client_id(client_id),
sequence_number(sequence_number),
session_id_(session_id),
namespace_or_v2_type_(namespace_or_v2_type),
message_body_(std::move(message_body)) {}
// static
std::unique_ptr<CastSession> CastSession::From(
const MediaSinkInternal& sink,
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;
}
if (session->app_id_ == kBackdropAppId) {
DVLOG(2) << sink.sink().id() << " is running the backdrop app";
return nullptr;
}
// Optional Session fields.
GetString(app_value, "statusText", &session->status_);
// The receiver label will be populated by each profile using
// |session->value|.
base::Value receiver_value = CreateReceiver(sink, std::string());
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);
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) {
DVLOG(2) << "Message is missing namespaces.";
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)) {
DVLOG(2) << "Missing namespace name.";
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;
std::string CastSession::GetRouteDescription() const {
return !status_.empty() ? status_ : display_name_;
}
void CastSession::UpdateSession(std::unique_ptr<CastSession> from) {
status_ = std::move(from->status_);
message_namespaces_ = std::move(from->message_namespaces_);
auto* status_text_value = from->value_.FindKey("statusText");
DCHECK(status_text_value);
value_.SetKey("statusText", std::move(*status_text_value));
auto* namespaces_value = from->value_.FindKey("namespaces");
DCHECK(namespaces_value);
value_.SetKey("namespaces", std::move(*namespaces_value));
auto* receiver_volume_value = from->value_.FindPath({"receiver", "volume"});
DCHECK(receiver_volume_value);
value_.SetPath({"receiver", "volume"}, std::move(*receiver_volume_value));
}
void CastSession::UpdateMedia(const base::Value& media) {
value_.SetKey("media", media.Clone());
}
blink::mojom::PresentationConnectionMessagePtr CreateReceiverActionCastMessage(
const std::string& client_id,
const MediaSinkInternal& sink,
const std::string& hash_token) {
return 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 CreateReceiverActionMessage(client_id, sink, hash_token,
kReceiverActionTypeStop);
}
blink::mojom::PresentationConnectionMessagePtr CreateNewSessionMessage(
const CastSession& session,
const std::string& client_id,
const MediaSinkInternal& sink,
const std::string& hash_token) {
return CreateSessionMessage(session, client_id, sink, hash_token,
CastInternalMessage::Type::kNewSession);
}
blink::mojom::PresentationConnectionMessagePtr CreateUpdateSessionMessage(
const CastSession& session,
const std::string& client_id,
const MediaSinkInternal& sink,
const std::string& hash_token) {
return CreateSessionMessage(session, client_id, sink, hash_token,
CastInternalMessage::Type::kUpdateSession);
}
blink::mojom::PresentationConnectionMessagePtr CreateAppMessageAck(
const std::string& client_id,
int sequence_number) {
return CreateMessageCommon(CastInternalMessage::Type::kAppMessage,
base::Value(), client_id, sequence_number);
}
blink::mojom::PresentationConnectionMessagePtr CreateAppMessage(
const std::string& session_id,
const std::string& client_id,
const cast_channel::CastMessage& cast_message) {
return CreateMessageCommon(CastInternalMessage::Type::kAppMessage,
CreateAppMessageBody(session_id, cast_message),
client_id);
}
blink::mojom::PresentationConnectionMessagePtr CreateV2Message(
const std::string& client_id,
const base::Value& payload,
base::Optional<int> sequence_number) {
return CreateMessageCommon(CastInternalMessage::Type::kV2Message,
payload.Clone(), client_id, sequence_number);
}
base::Value SupportedMediaRequestsToListValue(int media_requests) {
base::Value value(base::Value::Type::LIST);
auto& storage = value.GetList();
if (media_requests & 1)
storage.emplace_back("pause");
if (media_requests & 2)
storage.emplace_back("seek");
if (media_requests & 4)
storage.emplace_back("stream_volume");
if (media_requests & 8)
storage.emplace_back("stream_mute");
return value;
}
} // namespace media_router