Commit ef89b3f2 authored by Xiangjun Zhang's avatar Xiangjun Zhang Committed by Commit Bot

Mirroring service: Add cast message dispatcher.

Add MessageDispatcher to handle sending/receiving cast messages.

Bug: 734672
Change-Id: I1941ba72107b4629da8c47f0b726484d800b2891
Reviewed-on: https://chromium-review.googlesource.com/1008729
Commit-Queue: Xiangjun Zhang <xjz@chromium.org>
Reviewed-by: default avatarYuri Wiitala <miu@chromium.org>
Cr-Commit-Position: refs/heads/master@{#555622}
parent 79e486bd
......@@ -24,12 +24,18 @@ source_set("interface") {
source_set("service") {
sources = [
"message_dispatcher.cc",
"message_dispatcher.h",
"receiver_response.cc",
"receiver_response.h",
"rtp_stream.cc",
"rtp_stream.h",
"session.cc",
"session.h",
"udp_socket_client.cc",
"udp_socket_client.h",
"value_util.cc",
"value_util.h",
"video_capture_client.cc",
"video_capture_client.h",
]
......@@ -61,6 +67,8 @@ source_set("unittests") {
"fake_network_service.h",
"fake_video_capture_host.cc",
"fake_video_capture_host.h",
"message_dispatcher_unittest.cc",
"receiver_response_unittest.cc",
"rtp_stream_unittest.cc",
"session_unittest.cc",
"udp_socket_client_unittest.cc",
......
......@@ -5,6 +5,7 @@
#ifndef COMPONENTS_MIRRORING_SERVICE_INTERFACE_H_
#define COMPONENTS_MIRRORING_SERVICE_INTERFACE_H_
#include <string>
#include <vector>
#include "base/callback.h"
......@@ -34,6 +35,20 @@ enum SessionType {
AUDIO_AND_VIDEO,
};
constexpr char kRemotingNamespace[] = "urn:x-cast:com.google.cast.remoting";
constexpr char kWebRtcNamespace[] = "urn:x-cast:com.google.cast.webrtc";
struct CastMessage {
std::string message_namespace;
base::Value data;
};
class CastMessageChannel {
public:
virtual ~CastMessageChannel() {}
virtual void Send(const CastMessage& message) = 0;
};
class SessionClient {
public:
virtual ~SessionClient() {}
......@@ -49,7 +64,7 @@ class SessionClient {
virtual void GetVideoCaptureHost(
media::mojom::VideoCaptureHostRequest request) = 0;
virtual void GetNewWorkContext(
virtual void GetNetWorkContext(
network::mojom::NetworkContextRequest request) = 0;
// TODO(xjz): Add interface to get AudioCaptureHost.
// TODO(xjz): Add interface for HW encoder profiles query and VEA create
......
// 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 "components/mirroring/service/message_dispatcher.h"
#include "base/bind_helpers.h"
#include "base/json/json_writer.h"
#include "base/logging.h"
#include "base/rand_util.h"
namespace mirroring {
namespace {
std::string GetMessageString(const CastMessage& message) {
std::string message_string;
base::JSONWriter::Write(message.data, &message_string);
return message_string;
}
} // namespace
// Holds a request until |timeout| elapses or an acceptable response is
// received. When timeout, |response_callback| runs with an UNKNOWN type
// response.
class MessageDispatcher::RequestHolder {
public:
RequestHolder() {}
~RequestHolder() {
if (!response_callback_.is_null())
std::move(response_callback_).Run(ReceiverResponse());
}
void Start(const base::TimeDelta& timeout,
int32_t sequence_number,
OnceResponseCallback response_callback) {
response_callback_ = std::move(response_callback);
sequence_number_ = sequence_number;
DCHECK(!response_callback_.is_null());
timer_.Start(
FROM_HERE, timeout,
base::BindRepeating(&RequestHolder::SendResponse,
base::Unretained(this), ReceiverResponse()));
}
// Send |response| if the sequence number matches, or if the request times
// out, in which case the |response| is UNKNOWN type.
void SendResponse(const ReceiverResponse& response) {
if (!timer_.IsRunning() || response.sequence_number == sequence_number_)
std::move(response_callback_).Run(response);
// Ignore the response with mismatched sequence number.
}
private:
OnceResponseCallback response_callback_;
base::OneShotTimer timer_;
int32_t sequence_number_ = -1;
DISALLOW_COPY_AND_ASSIGN(RequestHolder);
};
MessageDispatcher::MessageDispatcher(CastMessageChannel* outbound_channel,
ErrorCallback error_callback)
: outbound_channel_(outbound_channel),
error_callback_(std::move(error_callback)),
last_sequence_number_(base::RandInt(0, 1e9)) {
DCHECK(outbound_channel_);
DCHECK(!error_callback_.is_null());
}
MessageDispatcher::~MessageDispatcher() {
// Prevent the re-entrant operations on |callback_map_|.
decltype(callback_map_) subscriptions;
subscriptions.swap(callback_map_);
subscriptions.clear();
}
void MessageDispatcher::Send(const CastMessage& message) {
if (message.message_namespace != kWebRtcNamespace &&
message.message_namespace != kRemotingNamespace) {
DVLOG(2) << "Ignore message with unknown namespace = "
<< message.message_namespace;
return; // Ignore message with wrong namespace.
}
if (message.data.is_none())
return; // Ignore null message.
ReceiverResponse response;
if (!response.Parse(message.data)) {
error_callback_.Run("Response parsing error. message=" +
GetMessageString(message));
return;
}
#if DCHECK_IS_ON()
if (response.type == ResponseType::RPC)
DCHECK_EQ(kRemotingNamespace, message.message_namespace);
else
DCHECK_EQ(kWebRtcNamespace, message.message_namespace);
#endif // DCHECK_IS_ON()
const auto callback_iter = callback_map_.find(response.type);
if (callback_iter == callback_map_.end()) {
error_callback_.Run("No callback subscribed. message=" +
GetMessageString(message));
return;
}
callback_iter->second.Run(response);
}
void MessageDispatcher::Subscribe(ResponseType type,
ResponseCallback callback) {
DCHECK(type != ResponseType::UNKNOWN);
DCHECK(!callback.is_null());
const auto insert_result =
callback_map_.emplace(std::make_pair(type, std::move(callback)));
DCHECK(insert_result.second);
}
void MessageDispatcher::Unsubscribe(ResponseType type) {
auto iter = callback_map_.find(type);
if (iter != callback_map_.end())
callback_map_.erase(iter);
}
int32_t MessageDispatcher::GetNextSeqNumber() {
// Skip 0, which is used by Cast receiver to indicate that the broadcast
// status message is not coming from a specific sender (it is an autonomous
// status change, not triggered by a command from any sender). Strange usage
// of 0 though; could be a null / optional field.
return ++last_sequence_number_;
}
void MessageDispatcher::SendOutboundMessage(const CastMessage& message) {
outbound_channel_->Send(message);
}
void MessageDispatcher::RequestReply(const CastMessage& message,
ResponseType response_type,
int32_t sequence_number,
const base::TimeDelta& timeout,
OnceResponseCallback callback) {
DCHECK(!callback.is_null());
DCHECK(timeout > base::TimeDelta());
RequestHolder* const request_holder = new RequestHolder();
request_holder->Start(
timeout, sequence_number,
base::BindOnce(
[](MessageDispatcher* dispatcher, ResponseType response_type,
OnceResponseCallback callback, const ReceiverResponse& response) {
dispatcher->Unsubscribe(response_type);
std::move(callback).Run(response);
},
this, response_type, std::move(callback)));
// |request_holder| keeps alive until the callback is unsubscribed.
Subscribe(response_type, base::BindRepeating(
[](RequestHolder* request_holder,
const ReceiverResponse& response) {
request_holder->SendResponse(response);
},
base::Owned(request_holder)));
SendOutboundMessage(message);
}
} // namespace mirroring
// 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 COMPONENTS_MIRRORING_SERVICE_MESSAGE_DISPATCHER_H_
#define COMPONENTS_MIRRORING_SERVICE_MESSAGE_DISPATCHER_H_
#include "base/callback.h"
#include "base/containers/flat_map.h"
#include "base/macros.h"
#include "components/mirroring/service/interface.h"
#include "components/mirroring/service/receiver_response.h"
namespace mirroring {
// Dispatches inbound/outbound messages. The outbound messages are sent out
// through |outbound_channel|, and the inbound messages are handled by this
// class.
class MessageDispatcher final : public CastMessageChannel {
public:
using ErrorCallback = base::RepeatingCallback<void(const std::string&)>;
// TODO(xjz): Also pass a CastMessageChannel interface request for inbound
// message channel.
MessageDispatcher(CastMessageChannel* outbound_channel,
ErrorCallback error_callback);
~MessageDispatcher() override;
using ResponseCallback =
base::RepeatingCallback<void(const ReceiverResponse& response)>;
// Registers/Unregisters callback for a certain type of responses.
void Subscribe(ResponseType type, ResponseCallback callback);
void Unsubscribe(ResponseType type);
using OnceResponseCallback =
base::OnceCallback<void(const ReceiverResponse& response)>;
// Sends the given message and subscribes to replies until an acceptable one
// is received or a timeout elapses. Message of the given response type is
// delivered to the supplied callback if the sequence number of the response
// matches |sequence_number|. If the timeout period elapses, the callback will
// be run once with an unknown type of |response|.
void RequestReply(const CastMessage& message,
ResponseType response_type,
int32_t sequence_number,
const base::TimeDelta& timeout,
OnceResponseCallback callback);
// Get the sequence number for the next outbound message. Never returns 0.
int32_t GetNextSeqNumber();
// Requests to send outbound |message|.
void SendOutboundMessage(const CastMessage& message);
private:
class RequestHolder;
// CastMessageChannel implementation. Handles inbound messages.
void Send(const CastMessage& message) override;
// Takes care of sending outbound messages.
CastMessageChannel* const outbound_channel_;
const ErrorCallback error_callback_;
int32_t last_sequence_number_;
// Holds callbacks for different types of responses.
base::flat_map<ResponseType, ResponseCallback> callback_map_;
DISALLOW_COPY_AND_ASSIGN(MessageDispatcher);
};
} // namespace mirroring
#endif // COMPONENTS_MIRRORING_SERVICE_MESSAGE_DISPATCHER_H_
This diff is collapsed.
// 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 "components/mirroring/service/receiver_response.h"
#include "base/base64.h"
#include "base/logging.h"
#include "base/strings/string_util.h"
#include "components/mirroring/service/value_util.h"
namespace mirroring {
namespace {
// Get the response type from the type string value in the JSON message.
ResponseType GetResponseType(const std::string& type) {
if (type == "ANSWER")
return ResponseType::ANSWER;
if (type == "STATUS_RESPONSE")
return ResponseType::STATUS_RESPONSE;
if (type == "CAPABILITIES_RESPONSE")
return ResponseType::CAPABILITIES_RESPONSE;
if (type == "RPC")
return ResponseType::RPC;
return ResponseType::UNKNOWN;
}
} // namespace
Answer::Answer()
: udp_port(-1), supports_get_status(false), cast_mode("mirroring") {}
Answer::~Answer() {}
Answer::Answer(const Answer& answer) = default;
bool Answer::Parse(const base::Value& raw_value) {
return (raw_value.is_dict() && GetInt(raw_value, "udpPort", &udp_port) &&
GetIntArray(raw_value, "ssrcs", &ssrcs) &&
GetIntArray(raw_value, "sendIndexes", &send_indexes) &&
GetString(raw_value, "IV", &iv) &&
GetBool(raw_value, "receiverGetStatus", &supports_get_status) &&
GetString(raw_value, "castMode", &cast_mode));
}
// ----------------------------------------------------------------------------
ReceiverStatus::ReceiverStatus() : wifi_snr(0) {}
ReceiverStatus::~ReceiverStatus() {}
ReceiverStatus::ReceiverStatus(const ReceiverStatus& status) = default;
bool ReceiverStatus::Parse(const base::Value& raw_value) {
return (raw_value.is_dict() && GetDouble(raw_value, "wifiSnr", &wifi_snr) &&
GetIntArray(raw_value, "wifiSpeed", &wifi_speed));
}
// ----------------------------------------------------------------------------
ReceiverKeySystem::ReceiverKeySystem() {}
ReceiverKeySystem::~ReceiverKeySystem() {}
ReceiverKeySystem::ReceiverKeySystem(
const ReceiverKeySystem& receiver_key_system) = default;
bool ReceiverKeySystem::Parse(const base::Value& raw_value) {
return (raw_value.is_dict() && GetString(raw_value, "keySystemName", &name) &&
GetStringArray(raw_value, "initDataTypes", &init_data_types) &&
GetStringArray(raw_value, "codecs", &codecs) &&
GetStringArray(raw_value, "secureCodecs", &secure_codecs) &&
GetStringArray(raw_value, "audioRobustness", &audio_robustness) &&
GetStringArray(raw_value, "videoRobustness", &video_robustness) &&
GetString(raw_value, "persistentLicenseSessionSupport",
&persistent_license_session_support) &&
GetString(raw_value, "persistentReleaseMessageSessionSupport",
&persistent_release_message_session_support) &&
GetString(raw_value, "persistentStateSupport",
&persistent_state_support) &&
GetString(raw_value, "distinctiveIdentifierSupport",
&distinctive_identifier_support));
}
// ----------------------------------------------------------------------------
ReceiverCapability::ReceiverCapability() {}
ReceiverCapability::~ReceiverCapability() {}
ReceiverCapability::ReceiverCapability(const ReceiverCapability& capabilities) =
default;
bool ReceiverCapability::Parse(const base::Value& raw_value) {
if (!raw_value.is_dict() ||
!GetStringArray(raw_value, "mediaCaps", &media_caps))
return false;
auto* found = raw_value.FindKey("keySystems");
if (!found)
return true;
for (const auto& key_system_value : found->GetList()) {
ReceiverKeySystem key_system;
if (!key_system.Parse(key_system_value))
return false;
key_systems.emplace_back(key_system);
}
return true;
}
// ----------------------------------------------------------------------------
ReceiverError::ReceiverError() : code(-1) {}
ReceiverError::~ReceiverError() {}
bool ReceiverError::Parse(const base::Value& raw_value) {
if (!raw_value.is_dict() || !GetInt(raw_value, "code", &code) ||
!GetString(raw_value, "description", &description))
return false;
auto* found = raw_value.FindKey("details");
if (found && !found->is_dict())
return false;
if (found)
details = found->Clone();
return true;
}
// ----------------------------------------------------------------------------
ReceiverResponse::ReceiverResponse()
: type(ResponseType::UNKNOWN), session_id(-1), sequence_number(-1) {}
ReceiverResponse::~ReceiverResponse() {}
ReceiverResponse::ReceiverResponse(ReceiverResponse&& receiver_response) =
default;
ReceiverResponse& ReceiverResponse::operator=(
ReceiverResponse&& receiver_response) = default;
bool ReceiverResponse::Parse(const base::Value& raw_value) {
if (!raw_value.is_dict() || !GetInt(raw_value, "sessionId", &session_id) ||
!GetInt(raw_value, "seqNum", &sequence_number) ||
!GetString(raw_value, "result", &result))
return false;
if (result == "error") {
auto* found = raw_value.FindKey("error");
if (found) {
error = std::make_unique<ReceiverError>();
if (!error->Parse(*found))
return false;
}
}
std::string message_type;
if (!GetString(raw_value, "type", &message_type))
return false;
// Convert |message_type| to uppercase.
message_type = base::ToUpperASCII(message_type);
type = GetResponseType(message_type);
if (type == ResponseType::UNKNOWN) {
DVLOG(2) << "Unknown response message type= " << message_type;
return false;
}
auto* found = raw_value.FindKey("answer");
if (found && !found->is_none()) {
answer = std::make_unique<Answer>();
if (!answer->Parse(*found))
return false;
}
found = raw_value.FindKey("status");
if (found && !found->is_none()) {
status = std::make_unique<ReceiverStatus>();
if (!status->Parse(*found))
return false;
}
found = raw_value.FindKey("capabilities");
if (found && !found->is_none()) {
capabilities = std::make_unique<ReceiverCapability>();
if (!capabilities->Parse(*found))
return false;
}
found = raw_value.FindKey("rpc");
if (found && !found->is_none()) {
// Decode the base64-encoded string.
if (!found->is_string() || !base::Base64Decode(found->GetString(), &rpc))
return false;
}
return true;
}
} // namespace mirroring
// 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 COMPONENTS_MIRRORING_SERVICE_RECEIVER_RESPONSE_H_
#define COMPONENTS_MIRRORING_SERVICE_RECEIVER_RESPONSE_H_
#include <memory>
#include <string>
#include <vector>
#include "base/values.h"
namespace mirroring {
// Receiver response message type.
enum ResponseType {
UNKNOWN,
ANSWER, // Response to OFFER message.
STATUS_RESPONSE, // Response to GET_STATUS message.
CAPABILITIES_RESPONSE, // Response to GET_CAPABILITIES message.
RPC, // Rpc binary messages. The payload is base64 encoded.
};
struct Answer {
Answer();
~Answer();
Answer(const Answer& answer);
bool Parse(const base::Value& raw_value);
// The UDP port used for all streams in this session.
int32_t udp_port;
// The indexes chosen from the OFFER message.
std::vector<int32_t> send_indexes;
// The RTP SSRC used to send the RTCP feedback of the stream, indicated by
// the |send_indexes| above.
std::vector<int32_t> ssrcs;
// A 128bit hex number containing the initialization vector for the crypto.
std::string iv;
// Indicates whether receiver supports the GET_STATUS command.
bool supports_get_status;
// "mirroring" for screen mirroring, or "remoting" for media remoting.
std::string cast_mode;
};
struct ReceiverStatus {
ReceiverStatus();
~ReceiverStatus();
ReceiverStatus(const ReceiverStatus& status);
bool Parse(const base::Value& raw_value);
// Current WiFi signal to noise ratio in decibels.
double wifi_snr;
// Min, max, average, and current bandwidth in bps in order of the WiFi link.
// Example: [1200, 1300, 1250, 1230].
std::vector<int32_t> wifi_speed;
};
struct ReceiverKeySystem {
ReceiverKeySystem();
~ReceiverKeySystem();
ReceiverKeySystem(const ReceiverKeySystem& receiver_key_system);
bool Parse(const base::Value& raw_value);
// Reverse URI (e.g. com.widevine.alpha).
std::string name;
// EME init data types (e.g. cenc).
std::vector<std::string> init_data_types;
// Codecs supported by key system. This will include AVC and VP8 on all
// Chromecasts.
std::vector<std::string> codecs;
// Codecs that are also hardware-secure.
std::vector<std::string> secure_codecs;
// Support levels for audio encryption robustness.
std::vector<std::string> audio_robustness;
// Support levels for video encryption robustness.
std::vector<std::string> video_robustness;
std::string persistent_license_session_support;
std::string persistent_release_message_session_support;
std::string persistent_state_support;
std::string distinctive_identifier_support;
};
struct ReceiverCapability {
ReceiverCapability();
~ReceiverCapability();
ReceiverCapability(const ReceiverCapability& capabilities);
bool Parse(const base::Value& raw_value);
// Set of capabilities (e.g., ac3, 4k, hevc, vp9, dolby_vision, etc.).
std::vector<std::string> media_caps;
std::vector<ReceiverKeySystem> key_systems;
};
struct ReceiverError {
ReceiverError();
~ReceiverError();
bool Parse(const base::Value& raw_value);
int32_t code;
std::string description;
base::Value details;
};
struct ReceiverResponse {
ReceiverResponse();
~ReceiverResponse();
ReceiverResponse(ReceiverResponse&& receiver_response);
ReceiverResponse& operator=(ReceiverResponse&& receiver_response);
bool Parse(const base::Value& raw_value);
ResponseType type;
// All messages have same |session_id| for each mirroring session. This value
// is provided by the media router provider.
int32_t session_id;
// This should be same as the value in the corresponding query/OFFER messages
// for non-rpc messages.
int32_t sequence_number;
std::string result; // "ok" or "error".
// Only one of the following has value, according to |type|.
std::unique_ptr<Answer> answer;
std::string rpc;
std::unique_ptr<ReceiverStatus> status;
std::unique_ptr<ReceiverCapability> capabilities;
// Can only be non-null when result is "error".
std::unique_ptr<ReceiverError> error;
};
} // namespace mirroring
#endif // COMPONENTS_MIRRORING_SERVICE_RECEIVER_RESPONSE_H_
This diff is collapsed.
......@@ -139,7 +139,7 @@ void Session::StartInternal(const net::IPEndPoint& receiver_endpoint,
base::ThreadTaskRunnerHandle::Get(), audio_encode_thread_,
video_encode_thread_);
network::mojom::NetworkContextPtr network_context;
client_->GetNewWorkContext(mojo::MakeRequest(&network_context));
client_->GetNetWorkContext(mojo::MakeRequest(&network_context));
auto udp_client = std::make_unique<UdpSocketClient>(
receiver_endpoint, std::move(network_context),
base::BindOnce(&Session::ReportError, weak_factory_.GetWeakPtr(),
......
......@@ -49,7 +49,7 @@ class SessionTest : public SessionClient, public ::testing::Test {
OnGetVideoCaptureHost();
}
void GetNewWorkContext(
void GetNetWorkContext(
network::mojom::NetworkContextRequest request) override {
network_context_ = std::make_unique<MockNetworkContext>(std::move(request));
OnGetNetworkContext();
......
// 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 "components/mirroring/service/value_util.h"
namespace mirroring {
bool GetInt(const base::Value& value, const std::string& key, int32_t* result) {
auto* found = value.FindKey(key);
if (!found || found->is_none())
return true;
if (found->is_int()) {
*result = found->GetInt();
return true;
}
return false;
}
bool GetDouble(const base::Value& value,
const std::string& key,
double* result) {
auto* found = value.FindKey(key);
if (!found || found->is_none())
return true;
if (found->is_double()) {
*result = found->GetDouble();
return true;
}
if (found->is_int()) {
*result = found->GetInt();
return true;
}
return false;
}
bool GetString(const base::Value& value,
const std::string& key,
std::string* result) {
auto* found = value.FindKey(key);
if (!found || found->is_none())
return true;
if (found->is_string()) {
*result = found->GetString();
return true;
}
return false;
}
bool GetBool(const base::Value& value, const std::string& key, bool* result) {
auto* found = value.FindKey(key);
if (!found || found->is_none())
return true;
if (found->is_bool()) {
*result = found->GetBool();
return true;
}
return false;
}
bool GetIntArray(const base::Value& value,
const std::string& key,
std::vector<int32_t>* result) {
auto* found = value.FindKey(key);
if (!found || found->is_none())
return true;
if (!found->is_list())
return false;
for (const auto& number_value : found->GetList()) {
if (number_value.is_int())
result->emplace_back(number_value.GetInt());
else
return false;
}
return true;
}
bool GetStringArray(const base::Value& value,
const std::string& key,
std::vector<std::string>* result) {
auto* found = value.FindKey(key);
if (!found || found->is_none())
return true;
if (!found->is_list())
return false;
for (const auto& string_value : found->GetList()) {
if (string_value.is_string())
result->emplace_back(string_value.GetString());
else
return false;
}
return true;
}
} // namespace mirroring
// 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 COMPONENTS_MIRRORING_SERVICE_VALUE_UTIL_H_
#define COMPONENTS_MIRRORING_SERVICE_VALUE_UTIL_H_
#include <string>
#include "base/values.h"
namespace mirroring {
// Read certain type of data from dictionary |value| if |key| exits. Return
// false if |key| exists and the type of the data mismatches. Return true
// otherwise.
bool GetInt(const base::Value& value, const std::string& key, int32_t* result);
bool GetDouble(const base::Value& value,
const std::string& key,
double* result);
bool GetString(const base::Value& value,
const std::string& key,
std::string* result);
bool GetBool(const base::Value& value, const std::string& key, bool* result);
bool GetIntArray(const base::Value& value,
const std::string& key,
std::vector<int32_t>* result);
bool GetStringArray(const base::Value& value,
const std::string& key,
std::vector<std::string>* result);
} // namespace mirroring
#endif // COMPONENTS_MIRRORING_SERVICE_VALUE_UTIL_H_
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