#include "remoting/client/plugin/chromoting_instance.h"
#include <string>
#include <vector>
#include "base/bind.h"
#include "base/callback.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/lazy_instance.h"
#include "base/logging.h"
#include "base/stringprintf.h"
#include "base/string_split.h"
#include "base/synchronization/lock.h"
#include "base/synchronization/waitable_event.h"
#include "base/threading/thread.h"
#include "base/values.h"
#include "jingle/glue/thread_wrapper.h"
#include "media/base/media.h"
#include "net/socket/ssl_server_socket.h"
#include "ppapi/cpp/completion_callback.h"
#include "ppapi/cpp/input_event.h"
#include "ppapi/cpp/mouse_cursor.h"
#include "ppapi/cpp/rect.h"
#include "remoting/base/constants.h"
#include "remoting/base/util.h"
#include "remoting/client/client_config.h"
#include "remoting/client/chromoting_client.h"
#include "remoting/client/frame_consumer_proxy.h"
#include "remoting/client/plugin/pepper_audio_player.h"
#include "remoting/client/plugin/pepper_input_handler.h"
#include "remoting/client/plugin/pepper_port_allocator.h"
#include "remoting/client/plugin/pepper_view.h"
#include "remoting/client/plugin/pepper_xmpp_proxy.h"
#include "remoting/client/rectangle_update_decoder.h"
#include "remoting/protocol/connection_to_host.h"
#include "remoting/protocol/host_stub.h"
#include "remoting/protocol/libjingle_transport_factory.h"
#if defined(PostMessage)
#undef PostMessage
#endif
namespace remoting {
namespace {
const int kBytesPerPixel = 4;
const int kPerfStatsIntervalMs = 1000;
std::string ConnectionStateToString(protocol::ConnectionToHost::State state) {
switch (state) {
case protocol::ConnectionToHost::INITIALIZING:
return "INITIALIZING";
case protocol::ConnectionToHost::CONNECTING:
return "CONNECTING";
case protocol::ConnectionToHost::CONNECTED:
return "CONNECTED";
case protocol::ConnectionToHost::CLOSED:
return "CLOSED";
case protocol::ConnectionToHost::FAILED:
return "FAILED";
}
NOTREACHED();
return "";
}
std::string ConnectionErrorToString(protocol::ErrorCode error) {
switch (error) {
case protocol::OK:
return "NONE";
case protocol::PEER_IS_OFFLINE:
return "HOST_IS_OFFLINE";
case protocol::SESSION_REJECTED:
case protocol::AUTHENTICATION_FAILED:
return "SESSION_REJECTED";
case protocol::INCOMPATIBLE_PROTOCOL:
return "INCOMPATIBLE_PROTOCOL";
case protocol::HOST_OVERLOAD:
return "HOST_OVERLOAD";
case protocol::CHANNEL_CONNECTION_ERROR:
case protocol::SIGNALING_ERROR:
case protocol::SIGNALING_TIMEOUT:
case protocol::UNKNOWN_ERROR:
return "NETWORK_FAILURE";
}
DLOG(FATAL) << "Unknown error code" << error;
return "";
}
bool g_logging_to_plugin = false;
bool g_has_logging_instance = false;
base::LazyInstance<scoped_refptr<base::SingleThreadTaskRunner> >::Leaky
g_logging_task_runner = LAZY_INSTANCE_INITIALIZER;
base::LazyInstance<base::WeakPtr<ChromotingInstance> >::Leaky
g_logging_instance = LAZY_INSTANCE_INITIALIZER;
base::LazyInstance<base::Lock>::Leaky
g_logging_lock = LAZY_INSTANCE_INITIALIZER;
logging::LogMessageHandlerFunction g_logging_old_handler = NULL;
}
const char ChromotingInstance::kApiFeatures[] =
"highQualityScaling injectKeyEvent sendClipboardItem remapKey trapKey "
"notifyClientDimensions pauseVideo";
bool ChromotingInstance::ParseAuthMethods(const std::string& auth_methods_str,
ClientConfig* config) {
std::vector<std::string> auth_methods;
base::SplitString(auth_methods_str, ',', &auth_methods);
for (std::vector<std::string>::iterator it = auth_methods.begin();
it != auth_methods.end(); ++it) {
protocol::AuthenticationMethod authentication_method =
protocol::AuthenticationMethod::FromString(*it);
if (authentication_method.is_valid())
config->authentication_methods.push_back(authentication_method);
}
if (config->authentication_methods.empty()) {
LOG(ERROR) << "No valid authentication methods specified.";
return false;
}
return true;
}
ChromotingInstance::ChromotingInstance(PP_Instance pp_instance)
: pp::Instance(pp_instance),
initialized_(false),
plugin_task_runner_(
new PluginThreadTaskRunner(&plugin_thread_delegate_)),
context_(plugin_task_runner_),
input_tracker_(&mouse_input_filter_),
#if defined(OS_MACOSX)
mac_key_event_processor_(&input_tracker_),
key_mapper_(&mac_key_event_processor_),
#else
key_mapper_(&input_tracker_),
#endif
input_handler_(&key_mapper_),
weak_factory_(ALLOW_THIS_IN_INITIALIZER_LIST(this)) {
RequestInputEvents(PP_INPUTEVENT_CLASS_MOUSE | PP_INPUTEVENT_CLASS_WHEEL);
RequestFilteringInputEvents(PP_INPUTEVENT_CLASS_KEYBOARD);
RegisterLoggingInstance();
scoped_ptr<base::DictionaryValue> data(new base::DictionaryValue());
data->SetInteger("apiVersion", kApiVersion);
data->SetString("apiFeatures", kApiFeatures);
data->SetInteger("apiMinVersion", kApiMinMessagingVersion);
PostChromotingMessage("hello", data.Pass());
}
ChromotingInstance::~ChromotingInstance() {
DCHECK(plugin_task_runner_->BelongsToCurrentThread());
UnregisterLoggingInstance();
view_.reset();
if (client_.get()) {
base::WaitableEvent done_event(true, false);
client_->Stop(base::Bind(&base::WaitableEvent::Signal,
base::Unretained(&done_event)));
done_event.Wait();
}
context_.Stop();
plugin_task_runner_->Detach();
}
bool ChromotingInstance::Init(uint32_t argc,
const char* argn[],
const char* argv[]) {
CHECK(!initialized_);
initialized_ = true;
VLOG(1) << "Started ChromotingInstance::Init";
if (!media::IsMediaLibraryInitialized()) {
LOG(ERROR) << "Media library not initialized.";
return false;
}
net::EnableSSLServerSockets();
context_.Start();
scoped_refptr<FrameConsumerProxy> consumer_proxy =
new FrameConsumerProxy(plugin_task_runner_);
rectangle_decoder_ = new RectangleUpdateDecoder(context_.main_task_runner(),
context_.decode_task_runner(),
consumer_proxy);
view_.reset(new PepperView(this, &context_, rectangle_decoder_.get()));
consumer_proxy->Attach(view_->AsWeakPtr());
return true;
}
void ChromotingInstance::HandleMessage(const pp::Var& message) {
if (!message.is_string()) {
LOG(ERROR) << "Received a message that is not a string.";
return;
}
scoped_ptr<base::Value> json(
base::JSONReader::Read(message.AsString(),
base::JSON_ALLOW_TRAILING_COMMAS));
base::DictionaryValue* message_dict = NULL;
std::string method;
base::DictionaryValue* data = NULL;
if (!json.get() ||
!json->GetAsDictionary(&message_dict) ||
!message_dict->GetString("method", &method) ||
!message_dict->GetDictionary("data", &data)) {
LOG(ERROR) << "Received invalid message:" << message.AsString();
return;
}
if (method == "connect") {
ClientConfig config;
std::string auth_methods;
if (!data->GetString("hostJid", &config.host_jid) ||
!data->GetString("hostPublicKey", &config.host_public_key) ||
!data->GetString("localJid", &config.local_jid) ||
!data->GetString("sharedSecret", &config.shared_secret) ||
!data->GetString("authenticationMethods", &auth_methods) ||
!ParseAuthMethods(auth_methods, &config) ||
!data->GetString("authenticationTag", &config.authentication_tag)) {
LOG(ERROR) << "Invalid connect() data.";
return;
}
Connect(config);
} else if (method == "disconnect") {
Disconnect();
} else if (method == "incomingIq") {
std::string iq;
if (!data->GetString("iq", &iq)) {
LOG(ERROR) << "Invalid onIq() data.";
return;
}
OnIncomingIq(iq);
} else if (method == "releaseAllKeys") {
ReleaseAllKeys();
} else if (method == "injectKeyEvent") {
int usb_keycode = 0;
bool is_pressed = false;
if (!data->GetInteger("usbKeycode", &usb_keycode) ||
!data->GetBoolean("pressed", &is_pressed)) {
LOG(ERROR) << "Invalid injectKeyEvent.";
return;
}
protocol::KeyEvent event;
event.set_usb_keycode(usb_keycode);
event.set_pressed(is_pressed);
InjectKeyEvent(event);
} else if (method == "remapKey") {
int from_keycode = 0;
int to_keycode = 0;
if (!data->GetInteger("fromKeycode", &from_keycode) ||
!data->GetInteger("toKeycode", &to_keycode)) {
LOG(ERROR) << "Invalid remapKey.";
return;
}
RemapKey(from_keycode, to_keycode);
} else if (method == "trapKey") {
int keycode = 0;
bool trap = false;
if (!data->GetInteger("keycode", &keycode) ||
!data->GetBoolean("trap", &trap)) {
LOG(ERROR) << "Invalid trapKey.";
return;
}
TrapKey(keycode, trap);
} else if (method == "sendClipboardItem") {
std::string mime_type;
std::string item;
if (!data->GetString("mimeType", &mime_type) ||
!data->GetString("item", &item)) {
LOG(ERROR) << "Invalid sendClipboardItem() data.";
return;
}
SendClipboardItem(mime_type, item);
} else if (method == "notifyClientDimensions") {
int width = 0;
int height = 0;
if (!data->GetInteger("width", &width) ||
!data->GetInteger("height", &height)) {
LOG(ERROR) << "Invalid notifyClientDimensions.";
return;
}
NotifyClientDimensions(width, height);
} else if (method == "pauseVideo") {
bool pause = false;
if (!data->GetBoolean("pause", &pause)) {
LOG(ERROR) << "Invalid pauseVideo.";
return;
}
PauseVideo(pause);
}
}
void ChromotingInstance::DidChangeView(const pp::View& view) {
DCHECK(plugin_task_runner_->BelongsToCurrentThread());
view_->SetView(view);
mouse_input_filter_.set_input_size(view_->get_view_size_dips());
}
bool ChromotingInstance::HandleInputEvent(const pp::InputEvent& event) {
DCHECK(plugin_task_runner_->BelongsToCurrentThread());
if (!IsConnected())
return false;
mouse_input_filter_.set_output_size(view_->get_screen_size());
return input_handler_.HandleInputEvent(event);
}
void ChromotingInstance::SetDesktopSize(const SkISize& size,
const SkIPoint& dpi) {
scoped_ptr<base::DictionaryValue> data(new base::DictionaryValue());
data->SetInteger("width", size.width());
data->SetInteger("height", size.height());
if (dpi.x())
data->SetInteger("x_dpi", dpi.x());
if (dpi.y())
data->SetInteger("y_dpi", dpi.y());
PostChromotingMessage("onDesktopSize", data.Pass());
}
void ChromotingInstance::OnConnectionState(
protocol::ConnectionToHost::State state,
protocol::ErrorCode error) {
scoped_ptr<base::DictionaryValue> data(new base::DictionaryValue());
data->SetString("state", ConnectionStateToString(state));
data->SetString("error", ConnectionErrorToString(error));
PostChromotingMessage("onConnectionStatus", data.Pass());
}
void ChromotingInstance::OnConnectionReady(bool ready) {
scoped_ptr<base::DictionaryValue> data(new base::DictionaryValue());
data->SetBoolean("ready", ready);
PostChromotingMessage("onConnectionReady", data.Pass());
}
protocol::ClipboardStub* ChromotingInstance::GetClipboardStub() {
return this;
}
protocol::CursorShapeStub* ChromotingInstance::GetCursorShapeStub() {
return this;
}
void ChromotingInstance::InjectClipboardEvent(
const protocol::ClipboardEvent& event) {
scoped_ptr<base::DictionaryValue> data(new base::DictionaryValue());
data->SetString("mimeType", event.mime_type());
data->SetString("item", event.data());
PostChromotingMessage("injectClipboardItem", data.Pass());
}
void ChromotingInstance::SetCursorShape(
const protocol::CursorShapeInfo& cursor_shape) {
if (!cursor_shape.has_data() ||
!cursor_shape.has_width() ||
!cursor_shape.has_height() ||
!cursor_shape.has_hotspot_x() ||
!cursor_shape.has_hotspot_y()) {
return;
}
if (pp::ImageData::GetNativeImageDataFormat() !=
PP_IMAGEDATAFORMAT_BGRA_PREMUL) {
VLOG(2) << "Unable to set cursor shape - non-native image format";
return;
}
int width = cursor_shape.width();
int height = cursor_shape.height();
if (width > 32 || height > 32) {
VLOG(2) << "Cursor too large for SetCursor: "
<< width << "x" << height << " > 32x32";
return;
}
int hotspot_x = cursor_shape.hotspot_x();
int hotspot_y = cursor_shape.hotspot_y();
pp::ImageData cursor_image(this, PP_IMAGEDATAFORMAT_BGRA_PREMUL,
pp::Size(width, height), false);
int bytes_per_row = width * kBytesPerPixel;
const uint8* src_row_data = reinterpret_cast<const uint8*>(
cursor_shape.data().data());
uint8* dst_row_data = reinterpret_cast<uint8*>(cursor_image.data());
for (int row = 0; row < height; row++) {
memcpy(dst_row_data, src_row_data, bytes_per_row);
src_row_data += bytes_per_row;
dst_row_data += cursor_image.stride();
}
pp::MouseCursor::SetCursor(this, PP_MOUSECURSOR_TYPE_CUSTOM,
cursor_image,
pp::Point(hotspot_x, hotspot_y));
}
void ChromotingInstance::OnFirstFrameReceived() {
scoped_ptr<base::DictionaryValue> data(new base::DictionaryValue());
PostChromotingMessage("onFirstFrameReceived", data.Pass());
}
void ChromotingInstance::Connect(const ClientConfig& config) {
DCHECK(plugin_task_runner_->BelongsToCurrentThread());
jingle_glue::JingleThreadWrapper::EnsureForCurrentMessageLoop();
host_connection_.reset(new protocol::ConnectionToHost(true));
scoped_ptr<AudioPlayer> audio_player(new PepperAudioPlayer(this));
client_.reset(new ChromotingClient(config, &context_,
host_connection_.get(), this,
rectangle_decoder_.get(),
audio_player.Pass()));
mouse_input_filter_.set_input_stub(host_connection_->input_stub());
mouse_input_filter_.set_input_size(view_->get_view_size_dips());
LOG(INFO) << "Connecting to " << config.host_jid
<< ". Local jid: " << config.local_jid << ".";
xmpp_proxy_ = new PepperXmppProxy(
base::Bind(&ChromotingInstance::SendOutgoingIq, AsWeakPtr()),
plugin_task_runner_, context_.main_task_runner());
scoped_ptr<cricket::HttpPortAllocatorBase> port_allocator(
PepperPortAllocator::Create(this));
scoped_ptr<protocol::TransportFactory> transport_factory(
new protocol::LibjingleTransportFactory(port_allocator.Pass(), false));
client_->Start(xmpp_proxy_, transport_factory.Pass());
plugin_task_runner_->PostDelayedTask(
FROM_HERE, base::Bind(&ChromotingInstance::SendPerfStats, AsWeakPtr()),
base::TimeDelta::FromMilliseconds(kPerfStatsIntervalMs));
}
void ChromotingInstance::Disconnect() {
DCHECK(plugin_task_runner_->BelongsToCurrentThread());
view_.reset();
LOG(INFO) << "Disconnecting from host.";
if (client_.get()) {
base::WaitableEvent done_event(true, false);
client_->Stop(base::Bind(&base::WaitableEvent::Signal,
base::Unretained(&done_event)));
done_event.Wait();
client_.reset();
}
mouse_input_filter_.set_input_stub(NULL);
host_connection_.reset();
}
void ChromotingInstance::OnIncomingIq(const std::string& iq) {
xmpp_proxy_->OnIq(iq);
}
void ChromotingInstance::ReleaseAllKeys() {
if (IsConnected())
input_tracker_.ReleaseAll();
}
void ChromotingInstance::InjectKeyEvent(const protocol::KeyEvent& event) {
if (IsConnected())
input_tracker_.InjectKeyEvent(event);
}
void ChromotingInstance::RemapKey(uint32 in_usb_keycode,
uint32 out_usb_keycode) {
key_mapper_.RemapKey(in_usb_keycode, out_usb_keycode);
}
void ChromotingInstance::TrapKey(uint32 usb_keycode, bool trap) {
key_mapper_.TrapKey(usb_keycode, trap);
}
void ChromotingInstance::SendClipboardItem(const std::string& mime_type,
const std::string& item) {
if (!IsConnected()) {
return;
}
protocol::ClipboardEvent event;
event.set_mime_type(mime_type);
event.set_data(item);
host_connection_->clipboard_stub()->InjectClipboardEvent(event);
}
void ChromotingInstance::NotifyClientDimensions(int width, int height) {
if (!IsConnected()) {
return;
}
protocol::ClientDimensions client_dimensions;
client_dimensions.set_width(width);
client_dimensions.set_height(height);
host_connection_->host_stub()->NotifyClientDimensions(client_dimensions);
}
void ChromotingInstance::PauseVideo(bool pause) {
if (!IsConnected()) {
return;
}
protocol::VideoControl video_control;
video_control.set_enable(!pause);
host_connection_->host_stub()->ControlVideo(video_control);
}
ChromotingStats* ChromotingInstance::GetStats() {
if (!client_.get())
return NULL;
return client_->GetStats();
}
void ChromotingInstance::PostChromotingMessage(
const std::string& method,
scoped_ptr<base::DictionaryValue> data) {
scoped_ptr<base::DictionaryValue> message(new base::DictionaryValue());
message->SetString("method", method);
message->Set("data", data.release());
std::string message_json;
base::JSONWriter::Write(message.get(), &message_json);
PostMessage(pp::Var(message_json));
}
void ChromotingInstance::SendTrappedKey(uint32 usb_keycode, bool pressed) {
scoped_ptr<base::DictionaryValue> data(new base::DictionaryValue());
data->SetInteger("usbKeycode", usb_keycode);
data->SetBoolean("pressed", pressed);
PostChromotingMessage("trappedKeyEvent", data.Pass());
}
void ChromotingInstance::SendOutgoingIq(const std::string& iq) {
scoped_ptr<base::DictionaryValue> data(new base::DictionaryValue());
data->SetString("iq", iq);
PostChromotingMessage("sendOutgoingIq", data.Pass());
}
void ChromotingInstance::SendPerfStats() {
if (!client_.get()) {
return;
}
plugin_task_runner_->PostDelayedTask(
FROM_HERE, base::Bind(&ChromotingInstance::SendPerfStats, AsWeakPtr()),
base::TimeDelta::FromMilliseconds(kPerfStatsIntervalMs));
scoped_ptr<base::DictionaryValue> data(new base::DictionaryValue());
ChromotingStats* stats = client_->GetStats();
data->SetDouble("videoBandwidth", stats->video_bandwidth()->Rate());
data->SetDouble("videoFrameRate", stats->video_frame_rate()->Rate());
data->SetDouble("captureLatency", stats->video_capture_ms()->Average());
data->SetDouble("encodeLatency", stats->video_encode_ms()->Average());
data->SetDouble("decodeLatency", stats->video_decode_ms()->Average());
data->SetDouble("renderLatency", stats->video_paint_ms()->Average());
data->SetDouble("roundtripLatency", stats->round_trip_ms()->Average());
PostChromotingMessage("onPerfStats", data.Pass());
}
void ChromotingInstance::RegisterLogMessageHandler() {
base::AutoLock lock(g_logging_lock.Get());
VLOG(1) << "Registering global log handler";
g_logging_old_handler = logging::GetLogMessageHandler();
logging::SetLogMessageHandler(&LogToUI);
}
void ChromotingInstance::RegisterLoggingInstance() {
base::AutoLock lock(g_logging_lock.Get());
g_logging_instance.Get() = weak_factory_.GetWeakPtr();
g_logging_task_runner.Get() = plugin_task_runner_;
g_has_logging_instance = true;
}
void ChromotingInstance::UnregisterLoggingInstance() {
base::AutoLock lock(g_logging_lock.Get());
if (this != g_logging_instance.Get().get())
return;
g_has_logging_instance = false;
g_logging_instance.Get().reset();
g_logging_task_runner.Get() = NULL;
VLOG(1) << "Unregistering global log handler";
}
bool ChromotingInstance::LogToUI(int severity, const char* file, int line,
size_t message_start,
const std::string& str) {
if (g_has_logging_instance) {
scoped_refptr<base::SingleThreadTaskRunner> logging_task_runner;
base::WeakPtr<ChromotingInstance> logging_instance;
{
base::AutoLock lock(g_logging_lock.Get());
if (!g_logging_task_runner.Get()->BelongsToCurrentThread() ||
!g_logging_to_plugin) {
logging_task_runner = g_logging_task_runner.Get();
logging_instance = g_logging_instance.Get();
}
}
if (logging_task_runner.get()) {
std::string message = remoting::GetTimestampString();
message += (str.c_str() + message_start);
logging_task_runner->PostTask(
FROM_HERE, base::Bind(&ChromotingInstance::ProcessLogToUI,
logging_instance, message));
}
}
if (g_logging_old_handler)
return (g_logging_old_handler)(severity, file, line, message_start, str);
return false;
}
void ChromotingInstance::ProcessLogToUI(const std::string& message) {
DCHECK(plugin_task_runner_->BelongsToCurrentThread());
g_logging_to_plugin = true;
scoped_ptr<base::DictionaryValue> data(new base::DictionaryValue());
data->SetString("message", message);
PostChromotingMessage("logDebugMessage", data.Pass());
g_logging_to_plugin = false;
}
bool ChromotingInstance::IsConnected() {
return host_connection_.get() &&
(host_connection_->state() == protocol::ConnectionToHost::CONNECTED);
}
}