1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "remoting/client/plugin/chromoting_instance.h"
12 #include <sys/mount.h>
13 #include <nacl_io/nacl_io.h>
16 #include "base/bind.h"
17 #include "base/callback.h"
18 #include "base/json/json_reader.h"
19 #include "base/json/json_writer.h"
20 #include "base/lazy_instance.h"
21 #include "base/logging.h"
22 #include "base/strings/string_split.h"
23 #include "base/strings/stringprintf.h"
24 #include "base/synchronization/lock.h"
25 #include "base/threading/thread.h"
26 #include "base/values.h"
27 #include "crypto/random.h"
28 #include "jingle/glue/thread_wrapper.h"
29 #include "media/base/yuv_convert.h"
30 #include "net/socket/ssl_server_socket.h"
31 #include "ppapi/cpp/completion_callback.h"
32 #include "ppapi/cpp/dev/url_util_dev.h"
33 #include "ppapi/cpp/image_data.h"
34 #include "ppapi/cpp/input_event.h"
35 #include "ppapi/cpp/rect.h"
36 #include "ppapi/cpp/var_array_buffer.h"
37 #include "ppapi/cpp/var_dictionary.h"
38 #include "remoting/base/constants.h"
39 #include "remoting/base/util.h"
40 #include "remoting/client/chromoting_client.h"
41 #include "remoting/client/frame_consumer_proxy.h"
42 #include "remoting/client/plugin/delegating_signal_strategy.h"
43 #include "remoting/client/plugin/media_source_video_renderer.h"
44 #include "remoting/client/plugin/normalizing_input_filter_cros.h"
45 #include "remoting/client/plugin/normalizing_input_filter_mac.h"
46 #include "remoting/client/plugin/pepper_audio_player.h"
47 #include "remoting/client/plugin/pepper_input_handler.h"
48 #include "remoting/client/plugin/pepper_port_allocator.h"
49 #include "remoting/client/plugin/pepper_view.h"
50 #include "remoting/client/software_video_renderer.h"
51 #include "remoting/client/token_fetcher_proxy.h"
52 #include "remoting/protocol/connection_to_host.h"
53 #include "remoting/protocol/host_stub.h"
54 #include "remoting/protocol/libjingle_transport_factory.h"
55 #include "third_party/libjingle/source/talk/base/helpers.h"
56 #include "third_party/libjingle/source/talk/base/ssladapter.h"
59 // Windows defines 'PostMessage', so we have to undef it.
60 #if defined(PostMessage)
68 // 32-bit BGRA is 4 bytes per pixel.
69 const int kBytesPerPixel
= 4;
71 #if defined(ARCH_CPU_LITTLE_ENDIAN)
72 const uint32_t kPixelAlphaMask
= 0xff000000;
73 #else // !defined(ARCH_CPU_LITTLE_ENDIAN)
74 const uint32_t kPixelAlphaMask
= 0x000000ff;
75 #endif // !defined(ARCH_CPU_LITTLE_ENDIAN)
77 // Default DPI to assume for old clients that use notifyClientResolution.
78 const int kDefaultDPI
= 96;
80 // Interval at which to sample performance statistics.
81 const int kPerfStatsIntervalMs
= 1000;
83 // URL scheme used by Chrome apps and extensions.
84 const char kChromeExtensionUrlScheme
[] = "chrome-extension";
86 // Maximum width and height of a mouse cursor supported by PPAPI.
87 const int kMaxCursorWidth
= 32;
88 const int kMaxCursorHeight
= 32;
90 #if defined(USE_OPENSSL)
91 // Size of the random seed blob used to initialize RNG in libjingle. Libjingle
92 // uses the seed only for OpenSSL builds. OpenSSL needs at least 32 bytes of
93 // entropy (see http://wiki.openssl.org/index.php/Random_Numbers), but stores
94 // 1039 bytes of state, so we initialize it with 1k or random data.
95 const int kRandomSeedSize
= 1024;
96 #endif // defined(USE_OPENSSL)
98 std::string
ConnectionStateToString(protocol::ConnectionToHost::State state
) {
99 // Values returned by this function must match the
100 // remoting.ClientSession.State enum in JS code.
102 case protocol::ConnectionToHost::INITIALIZING
:
103 return "INITIALIZING";
104 case protocol::ConnectionToHost::CONNECTING
:
106 case protocol::ConnectionToHost::AUTHENTICATED
:
107 // Report the authenticated state as 'CONNECTING' to avoid changing
108 // the interface between the plugin and webapp.
110 case protocol::ConnectionToHost::CONNECTED
:
112 case protocol::ConnectionToHost::CLOSED
:
114 case protocol::ConnectionToHost::FAILED
:
118 return std::string();
121 // TODO(sergeyu): Ideally we should just pass ErrorCode to the webapp
122 // and let it handle it, but it would be hard to fix it now because
123 // client plugin and webapp versions may not be in sync. It should be
124 // easy to do after we are finished moving the client plugin to NaCl.
125 std::string
ConnectionErrorToString(protocol::ErrorCode error
) {
126 // Values returned by this function must match the
127 // remoting.ClientSession.Error enum in JS code.
132 case protocol::PEER_IS_OFFLINE
:
133 return "HOST_IS_OFFLINE";
135 case protocol::SESSION_REJECTED
:
136 case protocol::AUTHENTICATION_FAILED
:
137 return "SESSION_REJECTED";
139 case protocol::INCOMPATIBLE_PROTOCOL
:
140 return "INCOMPATIBLE_PROTOCOL";
142 case protocol::HOST_OVERLOAD
:
143 return "HOST_OVERLOAD";
145 case protocol::CHANNEL_CONNECTION_ERROR
:
146 case protocol::SIGNALING_ERROR
:
147 case protocol::SIGNALING_TIMEOUT
:
148 case protocol::UNKNOWN_ERROR
:
149 return "NETWORK_FAILURE";
151 DLOG(FATAL
) << "Unknown error code" << error
;
152 return std::string();
155 // Returns true if |pixel| is not completely transparent.
156 bool IsVisiblePixel(uint32_t pixel
) {
157 return (pixel
& kPixelAlphaMask
) != 0;
160 // Returns true if there is at least one visible pixel in the given range.
161 bool IsVisibleRow(const uint32_t* begin
, const uint32_t* end
) {
162 return std::find_if(begin
, end
, &IsVisiblePixel
) != end
;
165 bool ParseAuthMethods(
166 const std::string
& auth_methods_str
,
167 std::vector
<protocol::AuthenticationMethod
>* auth_methods
) {
168 std::vector
<std::string
> parts
;
169 base::SplitString(auth_methods_str
, ',', &parts
);
170 for (std::vector
<std::string
>::iterator it
= parts
.begin();
171 it
!= parts
.end(); ++it
) {
172 protocol::AuthenticationMethod authentication_method
=
173 protocol::AuthenticationMethod::FromString(*it
);
174 if (authentication_method
.is_valid())
175 auth_methods
->push_back(authentication_method
);
177 if (auth_methods
->empty()) {
178 LOG(ERROR
) << "No valid authentication methods specified.";
185 // This flag blocks LOGs to the UI if we're already in the middle of logging
186 // to the UI. This prevents a potential infinite loop if we encounter an error
187 // while sending the log message to the UI.
188 bool g_logging_to_plugin
= false;
189 bool g_has_logging_instance
= false;
190 base::LazyInstance
<scoped_refptr
<base::SingleThreadTaskRunner
> >::Leaky
191 g_logging_task_runner
= LAZY_INSTANCE_INITIALIZER
;
192 base::LazyInstance
<base::WeakPtr
<ChromotingInstance
> >::Leaky
193 g_logging_instance
= LAZY_INSTANCE_INITIALIZER
;
194 base::LazyInstance
<base::Lock
>::Leaky
195 g_logging_lock
= LAZY_INSTANCE_INITIALIZER
;
196 logging::LogMessageHandlerFunction g_logging_old_handler
= NULL
;
200 // String sent in the "hello" message to the webapp to describe features.
201 const char ChromotingInstance::kApiFeatures
[] =
202 "highQualityScaling injectKeyEvent sendClipboardItem remapKey trapKey "
203 "notifyClientResolution pauseVideo pauseAudio asyncPin thirdPartyAuth "
204 "pinlessAuth extensionMessage allowMouseLock mediaSourceRendering "
207 const char ChromotingInstance::kRequestedCapabilities
[] = "";
208 const char ChromotingInstance::kSupportedCapabilities
[] = "desktopShape";
210 ChromotingInstance::ChromotingInstance(PP_Instance pp_instance
)
211 : pp::Instance(pp_instance
),
213 plugin_task_runner_(new PluginThreadTaskRunner(&plugin_thread_delegate_
)),
214 context_(plugin_task_runner_
.get()),
215 input_tracker_(&mouse_input_filter_
),
216 key_mapper_(&input_tracker_
),
217 input_handler_(this),
218 use_async_pin_dialog_(false),
219 use_media_source_rendering_(false),
220 delegate_large_cursors_(false),
221 weak_factory_(this) {
223 // In NaCl global resources need to be initialized differently because they
224 // are not shared with Chrome.
225 thread_task_runner_handle_
.reset(
226 new base::ThreadTaskRunnerHandle(plugin_task_runner_
));
227 thread_wrapper_
.reset(
228 new jingle_glue::JingleThreadWrapper(plugin_task_runner_
));
229 media::InitializeCPUSpecificYUVConversions();
231 jingle_glue::JingleThreadWrapper::EnsureForCurrentMessageLoop();
235 nacl_io_init_ppapi(pp_instance
, pp::Module::Get()->get_browser_interface());
236 mount("", "/etc", "memfs", 0, "");
237 mount("", "/usr", "memfs", 0, "");
240 RequestInputEvents(PP_INPUTEVENT_CLASS_MOUSE
| PP_INPUTEVENT_CLASS_WHEEL
);
241 RequestFilteringInputEvents(PP_INPUTEVENT_CLASS_KEYBOARD
);
243 // Resister this instance to handle debug log messsages.
244 RegisterLoggingInstance();
246 #if defined(USE_OPENSSL)
247 // Initialize random seed for libjingle. It's necessary only with OpenSSL.
248 char random_seed
[kRandomSeedSize
];
249 crypto::RandBytes(random_seed
, sizeof(random_seed
));
250 talk_base::InitRandom(random_seed
, sizeof(random_seed
));
252 // Libjingle's SSL implementation is not really used, but it has to be
253 // initialized for NSS builds to make sure that RNG is initialized in NSS,
254 // because libjingle uses it.
255 talk_base::InitializeSSL();
256 #endif // !defined(USE_OPENSSL)
258 // Send hello message.
259 scoped_ptr
<base::DictionaryValue
> data(new base::DictionaryValue());
260 data
->SetInteger("apiVersion", kApiVersion
);
261 data
->SetString("apiFeatures", kApiFeatures
);
262 data
->SetInteger("apiMinVersion", kApiMinMessagingVersion
);
263 data
->SetString("requestedCapabilities", kRequestedCapabilities
);
264 data
->SetString("supportedCapabilities", kSupportedCapabilities
);
266 PostLegacyJsonMessage("hello", data
.Pass());
269 ChromotingInstance::~ChromotingInstance() {
270 DCHECK(plugin_task_runner_
->BelongsToCurrentThread());
272 // Unregister this instance so that debug log messages will no longer be sent
273 // to it. This will stop all logging in all Chromoting instances.
274 UnregisterLoggingInstance();
276 // PepperView must be destroyed before the client.
277 view_weak_factory_
.reset();
282 plugin_task_runner_
->Quit();
284 // Ensure that nothing touches the plugin thread delegate after this point.
285 plugin_task_runner_
->DetachAndRunShutdownLoop();
287 // Stopping the context shuts down all chromoting threads.
291 bool ChromotingInstance::Init(uint32_t argc
,
293 const char* argv
[]) {
294 CHECK(!initialized_
);
297 VLOG(1) << "Started ChromotingInstance::Init";
299 // Check that the calling content is part of an app or extension. This is only
300 // necessary for non-PNaCl version of the plugin. Also PPB_URLUtil_Dev doesn't
301 // work in NaCl at the moment so the check fails in NaCl builds.
302 #if !defined(OS_NACL)
303 if (!IsCallerAppOrExtension()) {
304 LOG(ERROR
) << "Not an app or extension";
309 // Start all the threads.
315 void ChromotingInstance::HandleMessage(const pp::Var
& message
) {
316 if (!message
.is_string()) {
317 LOG(ERROR
) << "Received a message that is not a string.";
321 scoped_ptr
<base::Value
> json(
322 base::JSONReader::Read(message
.AsString(),
323 base::JSON_ALLOW_TRAILING_COMMAS
));
324 base::DictionaryValue
* message_dict
= NULL
;
326 base::DictionaryValue
* data
= NULL
;
328 !json
->GetAsDictionary(&message_dict
) ||
329 !message_dict
->GetString("method", &method
) ||
330 !message_dict
->GetDictionary("data", &data
)) {
331 LOG(ERROR
) << "Received invalid message:" << message
.AsString();
335 if (method
== "connect") {
336 HandleConnect(*data
);
337 } else if (method
== "disconnect") {
338 HandleDisconnect(*data
);
339 } else if (method
== "incomingIq") {
340 HandleOnIncomingIq(*data
);
341 } else if (method
== "releaseAllKeys") {
342 HandleReleaseAllKeys(*data
);
343 } else if (method
== "injectKeyEvent") {
344 HandleInjectKeyEvent(*data
);
345 } else if (method
== "remapKey") {
346 HandleRemapKey(*data
);
347 } else if (method
== "trapKey") {
348 HandleTrapKey(*data
);
349 } else if (method
== "sendClipboardItem") {
350 HandleSendClipboardItem(*data
);
351 } else if (method
== "notifyClientResolution") {
352 HandleNotifyClientResolution(*data
);
353 } else if (method
== "pauseVideo") {
354 HandlePauseVideo(*data
);
355 } else if (method
== "videoControl") {
356 HandleVideoControl(*data
);
357 } else if (method
== "pauseAudio") {
358 HandlePauseAudio(*data
);
359 } else if (method
== "useAsyncPinDialog") {
360 use_async_pin_dialog_
= true;
361 } else if (method
== "onPinFetched") {
362 HandleOnPinFetched(*data
);
363 } else if (method
== "onThirdPartyTokenFetched") {
364 HandleOnThirdPartyTokenFetched(*data
);
365 } else if (method
== "requestPairing") {
366 HandleRequestPairing(*data
);
367 } else if (method
== "extensionMessage") {
368 HandleExtensionMessage(*data
);
369 } else if (method
== "allowMouseLock") {
370 HandleAllowMouseLockMessage();
371 } else if (method
== "enableMediaSourceRendering") {
372 HandleEnableMediaSourceRendering();
373 } else if (method
== "sendMouseInputWhenUnfocused") {
374 HandleSendMouseInputWhenUnfocused();
375 } else if (method
== "delegateLargeCursors") {
376 HandleDelegateLargeCursors();
380 void ChromotingInstance::DidChangeFocus(bool has_focus
) {
381 DCHECK(plugin_task_runner_
->BelongsToCurrentThread());
386 input_handler_
.DidChangeFocus(has_focus
);
389 void ChromotingInstance::DidChangeView(const pp::View
& view
) {
390 DCHECK(plugin_task_runner_
->BelongsToCurrentThread());
393 mouse_input_filter_
.set_input_size(
394 webrtc::DesktopSize(view
.GetRect().width(), view
.GetRect().height()));
397 view_
->SetView(view
);
400 bool ChromotingInstance::HandleInputEvent(const pp::InputEvent
& event
) {
401 DCHECK(plugin_task_runner_
->BelongsToCurrentThread());
406 return input_handler_
.HandleInputEvent(event
);
409 void ChromotingInstance::SetDesktopSize(const webrtc::DesktopSize
& size
,
410 const webrtc::DesktopVector
& dpi
) {
411 mouse_input_filter_
.set_output_size(size
);
413 scoped_ptr
<base::DictionaryValue
> data(new base::DictionaryValue());
414 data
->SetInteger("width", size
.width());
415 data
->SetInteger("height", size
.height());
417 data
->SetInteger("x_dpi", dpi
.x());
419 data
->SetInteger("y_dpi", dpi
.y());
420 PostLegacyJsonMessage("onDesktopSize", data
.Pass());
423 void ChromotingInstance::SetDesktopShape(const webrtc::DesktopRegion
& shape
) {
424 if (desktop_shape_
&& shape
.Equals(*desktop_shape_
))
427 desktop_shape_
.reset(new webrtc::DesktopRegion(shape
));
429 scoped_ptr
<base::ListValue
> rects_value(new base::ListValue());
430 for (webrtc::DesktopRegion::Iterator
i(shape
); !i
.IsAtEnd(); i
.Advance()) {
431 const webrtc::DesktopRect
& rect
= i
.rect();
432 scoped_ptr
<base::ListValue
> rect_value(new base::ListValue());
433 rect_value
->AppendInteger(rect
.left());
434 rect_value
->AppendInteger(rect
.top());
435 rect_value
->AppendInteger(rect
.width());
436 rect_value
->AppendInteger(rect
.height());
437 rects_value
->Append(rect_value
.release());
440 scoped_ptr
<base::DictionaryValue
> data(new base::DictionaryValue());
441 data
->Set("rects", rects_value
.release());
442 PostLegacyJsonMessage("onDesktopShape", data
.Pass());
445 void ChromotingInstance::OnConnectionState(
446 protocol::ConnectionToHost::State state
,
447 protocol::ErrorCode error
) {
448 scoped_ptr
<base::DictionaryValue
> data(new base::DictionaryValue());
449 data
->SetString("state", ConnectionStateToString(state
));
450 data
->SetString("error", ConnectionErrorToString(error
));
451 PostLegacyJsonMessage("onConnectionStatus", data
.Pass());
454 void ChromotingInstance::FetchThirdPartyToken(
455 const GURL
& token_url
,
456 const std::string
& host_public_key
,
457 const std::string
& scope
,
458 base::WeakPtr
<TokenFetcherProxy
> token_fetcher_proxy
) {
459 // Once the Session object calls this function, it won't continue the
460 // authentication until the callback is called (or connection is canceled).
461 // So, it's impossible to reach this with a callback already registered.
462 DCHECK(!token_fetcher_proxy_
.get());
463 token_fetcher_proxy_
= token_fetcher_proxy
;
464 scoped_ptr
<base::DictionaryValue
> data(new base::DictionaryValue());
465 data
->SetString("tokenUrl", token_url
.spec());
466 data
->SetString("hostPublicKey", host_public_key
);
467 data
->SetString("scope", scope
);
468 PostLegacyJsonMessage("fetchThirdPartyToken", data
.Pass());
471 void ChromotingInstance::OnConnectionReady(bool ready
) {
472 scoped_ptr
<base::DictionaryValue
> data(new base::DictionaryValue());
473 data
->SetBoolean("ready", ready
);
474 PostLegacyJsonMessage("onConnectionReady", data
.Pass());
477 void ChromotingInstance::OnRouteChanged(const std::string
& channel_name
,
478 const protocol::TransportRoute
& route
) {
479 scoped_ptr
<base::DictionaryValue
> data(new base::DictionaryValue());
480 std::string message
= "Channel " + channel_name
+ " using " +
481 protocol::TransportRoute::GetTypeString(route
.type
) + " connection.";
482 data
->SetString("message", message
);
483 PostLegacyJsonMessage("logDebugMessage", data
.Pass());
486 void ChromotingInstance::SetCapabilities(const std::string
& capabilities
) {
487 scoped_ptr
<base::DictionaryValue
> data(new base::DictionaryValue());
488 data
->SetString("capabilities", capabilities
);
489 PostLegacyJsonMessage("setCapabilities", data
.Pass());
492 void ChromotingInstance::SetPairingResponse(
493 const protocol::PairingResponse
& pairing_response
) {
494 scoped_ptr
<base::DictionaryValue
> data(new base::DictionaryValue());
495 data
->SetString("clientId", pairing_response
.client_id());
496 data
->SetString("sharedSecret", pairing_response
.shared_secret());
497 PostLegacyJsonMessage("pairingResponse", data
.Pass());
500 void ChromotingInstance::DeliverHostMessage(
501 const protocol::ExtensionMessage
& message
) {
502 scoped_ptr
<base::DictionaryValue
> data(new base::DictionaryValue());
503 data
->SetString("type", message
.type());
504 data
->SetString("data", message
.data());
505 PostLegacyJsonMessage("extensionMessage", data
.Pass());
508 void ChromotingInstance::FetchSecretFromDialog(
509 bool pairing_supported
,
510 const protocol::SecretFetchedCallback
& secret_fetched_callback
) {
511 // Once the Session object calls this function, it won't continue the
512 // authentication until the callback is called (or connection is canceled).
513 // So, it's impossible to reach this with a callback already registered.
514 DCHECK(secret_fetched_callback_
.is_null());
515 secret_fetched_callback_
= secret_fetched_callback
;
516 scoped_ptr
<base::DictionaryValue
> data(new base::DictionaryValue());
517 data
->SetBoolean("pairingSupported", pairing_supported
);
518 PostLegacyJsonMessage("fetchPin", data
.Pass());
521 void ChromotingInstance::FetchSecretFromString(
522 const std::string
& shared_secret
,
523 bool pairing_supported
,
524 const protocol::SecretFetchedCallback
& secret_fetched_callback
) {
525 secret_fetched_callback
.Run(shared_secret
);
528 protocol::ClipboardStub
* ChromotingInstance::GetClipboardStub() {
529 // TODO(sergeyu): Move clipboard handling to a separate class.
534 protocol::CursorShapeStub
* ChromotingInstance::GetCursorShapeStub() {
535 // TODO(sergeyu): Move cursor shape code to a separate class.
540 void ChromotingInstance::InjectClipboardEvent(
541 const protocol::ClipboardEvent
& event
) {
542 scoped_ptr
<base::DictionaryValue
> data(new base::DictionaryValue());
543 data
->SetString("mimeType", event
.mime_type());
544 data
->SetString("item", event
.data());
545 PostLegacyJsonMessage("injectClipboardItem", data
.Pass());
548 void ChromotingInstance::SetCursorShape(
549 const protocol::CursorShapeInfo
& cursor_shape
) {
550 COMPILE_ASSERT(sizeof(uint32_t) == kBytesPerPixel
, rgba_pixels_are_32bit
);
552 // pp::MouseCursor requires image to be in the native format.
553 if (pp::ImageData::GetNativeImageDataFormat() !=
554 PP_IMAGEDATAFORMAT_BGRA_PREMUL
) {
555 LOG(WARNING
) << "Unable to set cursor shape - native image format is not"
556 " premultiplied BGRA";
560 int width
= cursor_shape
.width();
561 int height
= cursor_shape
.height();
563 int hotspot_x
= cursor_shape
.hotspot_x();
564 int hotspot_y
= cursor_shape
.hotspot_y();
565 int bytes_per_row
= width
* kBytesPerPixel
;
566 int src_stride
= width
;
567 const uint32_t* src_row_data
= reinterpret_cast<const uint32_t*>(
568 cursor_shape
.data().data());
569 const uint32_t* src_row_data_end
= src_row_data
+ src_stride
* height
;
571 scoped_ptr
<pp::ImageData
> cursor_image
;
572 pp::Point cursor_hotspot
;
574 // Check if the cursor is visible.
575 if (IsVisibleRow(src_row_data
, src_row_data_end
)) {
576 // If the cursor exceeds the size permitted by PPAPI then crop it, keeping
577 // the hotspot as close to the center of the new cursor shape as possible.
578 if (height
> kMaxCursorHeight
&& !delegate_large_cursors_
) {
579 int y
= hotspot_y
- (kMaxCursorHeight
/ 2);
581 y
= std::min(y
, height
- kMaxCursorHeight
);
583 src_row_data
+= src_stride
* y
;
584 height
= kMaxCursorHeight
;
587 if (width
> kMaxCursorWidth
&& !delegate_large_cursors_
) {
588 int x
= hotspot_x
- (kMaxCursorWidth
/ 2);
590 x
= std::min(x
, height
- kMaxCursorWidth
);
593 width
= kMaxCursorWidth
;
594 bytes_per_row
= width
* kBytesPerPixel
;
598 cursor_image
.reset(new pp::ImageData(this, PP_IMAGEDATAFORMAT_BGRA_PREMUL
,
599 pp::Size(width
, height
), false));
600 cursor_hotspot
= pp::Point(hotspot_x
, hotspot_y
);
602 uint8
* dst_row_data
= reinterpret_cast<uint8
*>(cursor_image
->data());
603 for (int row
= 0; row
< height
; row
++) {
604 memcpy(dst_row_data
, src_row_data
, bytes_per_row
);
605 src_row_data
+= src_stride
;
606 dst_row_data
+= cursor_image
->stride();
610 if (height
> kMaxCursorHeight
|| width
> kMaxCursorWidth
) {
611 DCHECK(delegate_large_cursors_
);
612 size_t buffer_size
= height
* bytes_per_row
;
613 pp::VarArrayBuffer
array_buffer(buffer_size
);
614 void* dst
= array_buffer
.Map();
615 memcpy(dst
, cursor_image
->data(), buffer_size
);
616 array_buffer
.Unmap();
617 pp::VarDictionary dictionary
;
618 dictionary
.Set(pp::Var("width"), width
);
619 dictionary
.Set(pp::Var("height"), height
);
620 dictionary
.Set(pp::Var("hotspotX"), cursor_hotspot
.x());
621 dictionary
.Set(pp::Var("hotspotY"), cursor_hotspot
.y());
622 dictionary
.Set(pp::Var("data"), array_buffer
);
623 PostChromotingMessage("setCursorShape", dictionary
);
624 input_handler_
.SetMouseCursor(scoped_ptr
<pp::ImageData
>(), cursor_hotspot
);
626 if (delegate_large_cursors_
) {
627 pp::VarDictionary dictionary
;
628 PostChromotingMessage("unsetCursorShape", dictionary
);
630 input_handler_
.SetMouseCursor(cursor_image
.Pass(), cursor_hotspot
);
634 void ChromotingInstance::OnFirstFrameReceived() {
635 scoped_ptr
<base::DictionaryValue
> data(new base::DictionaryValue());
636 PostLegacyJsonMessage("onFirstFrameReceived", data
.Pass());
639 void ChromotingInstance::HandleConnect(const base::DictionaryValue
& data
) {
640 std::string local_jid
;
641 std::string host_jid
;
642 std::string host_public_key
;
643 std::string auth_methods_str
;
644 std::string authentication_tag
;
645 std::vector
<protocol::AuthenticationMethod
> auth_methods
;
646 if (!data
.GetString("hostJid", &host_jid
) ||
647 !data
.GetString("hostPublicKey", &host_public_key
) ||
648 !data
.GetString("localJid", &local_jid
) ||
649 !data
.GetString("authenticationMethods", &auth_methods_str
) ||
650 !ParseAuthMethods(auth_methods_str
, &auth_methods
) ||
651 !data
.GetString("authenticationTag", &authentication_tag
)) {
652 LOG(ERROR
) << "Invalid connect() data.";
656 std::string client_pairing_id
;
657 data
.GetString("clientPairingId", &client_pairing_id
);
658 std::string client_paired_secret
;
659 data
.GetString("clientPairedSecret", &client_paired_secret
);
661 protocol::FetchSecretCallback fetch_secret_callback
;
662 if (use_async_pin_dialog_
) {
663 fetch_secret_callback
= base::Bind(
664 &ChromotingInstance::FetchSecretFromDialog
, weak_factory_
.GetWeakPtr());
666 std::string shared_secret
;
667 if (!data
.GetString("sharedSecret", &shared_secret
)) {
668 LOG(ERROR
) << "sharedSecret not specified in connect().";
671 fetch_secret_callback
=
672 base::Bind(&ChromotingInstance::FetchSecretFromString
, shared_secret
);
675 // Read the list of capabilities, if any.
676 std::string capabilities
;
677 if (data
.HasKey("capabilities")) {
678 if (!data
.GetString("capabilities", &capabilities
)) {
679 LOG(ERROR
) << "Invalid connect() data.";
684 VLOG(0) << "Connecting to " << host_jid
685 << ". Local jid: " << local_jid
<< ".";
688 std::string key_filter
;
689 if (!data
.GetString("keyFilter", &key_filter
)) {
691 normalizing_input_filter_
.reset(new protocol::InputFilter(&key_mapper_
));
692 } else if (key_filter
== "mac") {
693 normalizing_input_filter_
.reset(
694 new NormalizingInputFilterMac(&key_mapper_
));
695 } else if (key_filter
== "cros") {
696 normalizing_input_filter_
.reset(
697 new NormalizingInputFilterCros(&key_mapper_
));
699 DCHECK(key_filter
.empty());
700 normalizing_input_filter_
.reset(new protocol::InputFilter(&key_mapper_
));
702 #elif defined(OS_MACOSX)
703 normalizing_input_filter_
.reset(new NormalizingInputFilterMac(&key_mapper_
));
704 #elif defined(OS_CHROMEOS)
705 normalizing_input_filter_
.reset(new NormalizingInputFilterCros(&key_mapper_
));
707 normalizing_input_filter_
.reset(new protocol::InputFilter(&key_mapper_
));
709 input_handler_
.set_input_stub(normalizing_input_filter_
.get());
711 if (use_media_source_rendering_
) {
712 video_renderer_
.reset(new MediaSourceVideoRenderer(this));
714 view_
.reset(new PepperView(this, &context_
));
715 view_weak_factory_
.reset(
716 new base::WeakPtrFactory
<FrameConsumer
>(view_
.get()));
718 // SoftwareVideoRenderer runs on a separate thread so for now we wrap
719 // PepperView with a ref-counted proxy object.
720 scoped_refptr
<FrameConsumerProxy
> consumer_proxy
=
721 new FrameConsumerProxy(plugin_task_runner_
,
722 view_weak_factory_
->GetWeakPtr());
724 SoftwareVideoRenderer
* renderer
=
725 new SoftwareVideoRenderer(context_
.main_task_runner(),
726 context_
.decode_task_runner(),
728 view_
->Initialize(renderer
);
729 if (!plugin_view_
.is_null())
730 view_
->SetView(plugin_view_
);
731 video_renderer_
.reset(renderer
);
734 scoped_ptr
<AudioPlayer
> audio_player(new PepperAudioPlayer(this));
735 client_
.reset(new ChromotingClient(&context_
, this, video_renderer_
.get(),
736 audio_player
.Pass()));
738 // Connect the input pipeline to the protocol stub & initialize components.
739 mouse_input_filter_
.set_input_stub(client_
->input_stub());
740 if (!plugin_view_
.is_null()) {
741 mouse_input_filter_
.set_input_size(webrtc::DesktopSize(
742 plugin_view_
.GetRect().width(), plugin_view_
.GetRect().height()));
745 // Setup the signal strategy.
746 signal_strategy_
.reset(new DelegatingSignalStrategy(
747 local_jid
, base::Bind(&ChromotingInstance::SendOutgoingIq
,
748 weak_factory_
.GetWeakPtr())));
750 // Create TransportFactory.
751 scoped_ptr
<protocol::TransportFactory
> transport_factory(
752 new protocol::LibjingleTransportFactory(
753 signal_strategy_
.get(),
754 PepperPortAllocator::Create(this)
755 .PassAs
<cricket::HttpPortAllocatorBase
>(),
756 protocol::NetworkSettings(
757 protocol::NetworkSettings::NAT_TRAVERSAL_FULL
)));
759 // Create Authenticator.
760 scoped_ptr
<protocol::ThirdPartyClientAuthenticator::TokenFetcher
>
761 token_fetcher(new TokenFetcherProxy(
762 base::Bind(&ChromotingInstance::FetchThirdPartyToken
,
763 weak_factory_
.GetWeakPtr()),
765 scoped_ptr
<protocol::Authenticator
> authenticator(
766 new protocol::NegotiatingClientAuthenticator(
767 client_pairing_id
, client_paired_secret
, authentication_tag
,
768 fetch_secret_callback
, token_fetcher
.Pass(), auth_methods
));
770 // Kick off the connection.
771 client_
->Start(signal_strategy_
.get(), authenticator
.Pass(),
772 transport_factory
.Pass(), host_jid
, capabilities
);
774 // Start timer that periodically sends perf stats.
775 plugin_task_runner_
->PostDelayedTask(
776 FROM_HERE
, base::Bind(&ChromotingInstance::SendPerfStats
,
777 weak_factory_
.GetWeakPtr()),
778 base::TimeDelta::FromMilliseconds(kPerfStatsIntervalMs
));
781 void ChromotingInstance::HandleDisconnect(const base::DictionaryValue
& data
) {
782 DCHECK(plugin_task_runner_
->BelongsToCurrentThread());
784 // PepperView must be destroyed before the client.
785 view_weak_factory_
.reset();
788 VLOG(0) << "Disconnecting from host.";
790 // Disconnect the input pipeline and teardown the connection.
791 mouse_input_filter_
.set_input_stub(NULL
);
795 void ChromotingInstance::HandleOnIncomingIq(const base::DictionaryValue
& data
) {
797 if (!data
.GetString("iq", &iq
)) {
798 LOG(ERROR
) << "Invalid incomingIq() data.";
802 // Just ignore the message if it's received before Connect() is called. It's
803 // likely to be a leftover from a previous session, so it's safe to ignore it.
804 if (signal_strategy_
)
805 signal_strategy_
->OnIncomingMessage(iq
);
808 void ChromotingInstance::HandleReleaseAllKeys(
809 const base::DictionaryValue
& data
) {
811 input_tracker_
.ReleaseAll();
814 void ChromotingInstance::HandleInjectKeyEvent(
815 const base::DictionaryValue
& data
) {
817 bool is_pressed
= false;
818 if (!data
.GetInteger("usbKeycode", &usb_keycode
) ||
819 !data
.GetBoolean("pressed", &is_pressed
)) {
820 LOG(ERROR
) << "Invalid injectKeyEvent.";
824 protocol::KeyEvent event
;
825 event
.set_usb_keycode(usb_keycode
);
826 event
.set_pressed(is_pressed
);
828 // Inject after the KeyEventMapper, so the event won't get mapped or trapped.
830 input_tracker_
.InjectKeyEvent(event
);
833 void ChromotingInstance::HandleRemapKey(const base::DictionaryValue
& data
) {
834 int from_keycode
= 0;
836 if (!data
.GetInteger("fromKeycode", &from_keycode
) ||
837 !data
.GetInteger("toKeycode", &to_keycode
)) {
838 LOG(ERROR
) << "Invalid remapKey.";
842 key_mapper_
.RemapKey(from_keycode
, to_keycode
);
845 void ChromotingInstance::HandleTrapKey(const base::DictionaryValue
& data
) {
848 if (!data
.GetInteger("keycode", &keycode
) ||
849 !data
.GetBoolean("trap", &trap
)) {
850 LOG(ERROR
) << "Invalid trapKey.";
854 key_mapper_
.TrapKey(keycode
, trap
);
857 void ChromotingInstance::HandleSendClipboardItem(
858 const base::DictionaryValue
& data
) {
859 std::string mime_type
;
861 if (!data
.GetString("mimeType", &mime_type
) ||
862 !data
.GetString("item", &item
)) {
863 LOG(ERROR
) << "Invalid sendClipboardItem data.";
866 if (!IsConnected()) {
869 protocol::ClipboardEvent event
;
870 event
.set_mime_type(mime_type
);
871 event
.set_data(item
);
872 client_
->clipboard_forwarder()->InjectClipboardEvent(event
);
875 void ChromotingInstance::HandleNotifyClientResolution(
876 const base::DictionaryValue
& data
) {
879 int x_dpi
= kDefaultDPI
;
880 int y_dpi
= kDefaultDPI
;
881 if (!data
.GetInteger("width", &width
) ||
882 !data
.GetInteger("height", &height
) ||
883 !data
.GetInteger("x_dpi", &x_dpi
) ||
884 !data
.GetInteger("y_dpi", &y_dpi
) ||
885 width
<= 0 || height
<= 0 ||
886 x_dpi
<= 0 || y_dpi
<= 0) {
887 LOG(ERROR
) << "Invalid notifyClientResolution.";
891 if (!IsConnected()) {
895 protocol::ClientResolution client_resolution
;
896 client_resolution
.set_width(width
);
897 client_resolution
.set_height(height
);
898 client_resolution
.set_x_dpi(x_dpi
);
899 client_resolution
.set_y_dpi(y_dpi
);
901 // Include the legacy width & height in DIPs for use by older hosts.
902 client_resolution
.set_dips_width((width
* kDefaultDPI
) / x_dpi
);
903 client_resolution
.set_dips_height((height
* kDefaultDPI
) / y_dpi
);
905 client_
->host_stub()->NotifyClientResolution(client_resolution
);
908 void ChromotingInstance::HandlePauseVideo(const base::DictionaryValue
& data
) {
909 if (!data
.HasKey("pause")) {
910 LOG(ERROR
) << "Invalid pauseVideo.";
913 HandleVideoControl(data
);
916 void ChromotingInstance::HandleVideoControl(const base::DictionaryValue
& data
) {
917 protocol::VideoControl video_control
;
918 bool pause_video
= false;
919 if (data
.GetBoolean("pause", &pause_video
)) {
920 video_control
.set_enable(!pause_video
);
922 bool lossless_encode
= false;
923 if (data
.GetBoolean("losslessEncode", &lossless_encode
)) {
924 video_control
.set_lossless_encode(lossless_encode
);
926 bool lossless_color
= false;
927 if (data
.GetBoolean("losslessColor", &lossless_color
)) {
928 video_control
.set_lossless_color(lossless_color
);
930 if (!IsConnected()) {
933 client_
->host_stub()->ControlVideo(video_control
);
936 void ChromotingInstance::HandlePauseAudio(const base::DictionaryValue
& data
) {
938 if (!data
.GetBoolean("pause", &pause
)) {
939 LOG(ERROR
) << "Invalid pauseAudio.";
942 if (!IsConnected()) {
945 protocol::AudioControl audio_control
;
946 audio_control
.set_enable(!pause
);
947 client_
->host_stub()->ControlAudio(audio_control
);
949 void ChromotingInstance::HandleOnPinFetched(const base::DictionaryValue
& data
) {
951 if (!data
.GetString("pin", &pin
)) {
952 LOG(ERROR
) << "Invalid onPinFetched.";
955 if (!secret_fetched_callback_
.is_null()) {
956 secret_fetched_callback_
.Run(pin
);
957 secret_fetched_callback_
.Reset();
959 LOG(WARNING
) << "Ignored OnPinFetched received without a pending fetch.";
963 void ChromotingInstance::HandleOnThirdPartyTokenFetched(
964 const base::DictionaryValue
& data
) {
966 std::string shared_secret
;
967 if (!data
.GetString("token", &token
) ||
968 !data
.GetString("sharedSecret", &shared_secret
)) {
969 LOG(ERROR
) << "Invalid onThirdPartyTokenFetched data.";
972 if (token_fetcher_proxy_
.get()) {
973 token_fetcher_proxy_
->OnTokenFetched(token
, shared_secret
);
974 token_fetcher_proxy_
.reset();
976 LOG(WARNING
) << "Ignored OnThirdPartyTokenFetched without a pending fetch.";
980 void ChromotingInstance::HandleRequestPairing(
981 const base::DictionaryValue
& data
) {
982 std::string client_name
;
983 if (!data
.GetString("clientName", &client_name
)) {
984 LOG(ERROR
) << "Invalid requestPairing";
987 if (!IsConnected()) {
990 protocol::PairingRequest pairing_request
;
991 pairing_request
.set_client_name(client_name
);
992 client_
->host_stub()->RequestPairing(pairing_request
);
995 void ChromotingInstance::HandleExtensionMessage(
996 const base::DictionaryValue
& data
) {
998 std::string message_data
;
999 if (!data
.GetString("type", &type
) ||
1000 !data
.GetString("data", &message_data
)) {
1001 LOG(ERROR
) << "Invalid extensionMessage.";
1004 if (!IsConnected()) {
1007 protocol::ExtensionMessage message
;
1008 message
.set_type(type
);
1009 message
.set_data(message_data
);
1010 client_
->host_stub()->DeliverClientMessage(message
);
1013 void ChromotingInstance::HandleAllowMouseLockMessage() {
1014 input_handler_
.AllowMouseLock();
1017 void ChromotingInstance::HandleEnableMediaSourceRendering() {
1018 use_media_source_rendering_
= true;
1021 void ChromotingInstance::HandleSendMouseInputWhenUnfocused() {
1022 input_handler_
.set_send_mouse_input_when_unfocused(true);
1025 void ChromotingInstance::HandleDelegateLargeCursors() {
1026 delegate_large_cursors_
= true;
1029 ChromotingStats
* ChromotingInstance::GetStats() {
1030 if (!video_renderer_
.get())
1032 return video_renderer_
->GetStats();
1035 void ChromotingInstance::PostChromotingMessage(const std::string
& method
,
1036 const pp::VarDictionary
& data
) {
1037 pp::VarDictionary message
;
1038 message
.Set(pp::Var("method"), pp::Var(method
));
1039 message
.Set(pp::Var("data"), data
);
1040 PostMessage(message
);
1043 void ChromotingInstance::PostLegacyJsonMessage(
1044 const std::string
& method
,
1045 scoped_ptr
<base::DictionaryValue
> data
) {
1046 scoped_ptr
<base::DictionaryValue
> message(new base::DictionaryValue());
1047 message
->SetString("method", method
);
1048 message
->Set("data", data
.release());
1050 std::string message_json
;
1051 base::JSONWriter::Write(message
.get(), &message_json
);
1052 PostMessage(pp::Var(message_json
));
1055 void ChromotingInstance::SendTrappedKey(uint32 usb_keycode
, bool pressed
) {
1056 scoped_ptr
<base::DictionaryValue
> data(new base::DictionaryValue());
1057 data
->SetInteger("usbKeycode", usb_keycode
);
1058 data
->SetBoolean("pressed", pressed
);
1059 PostLegacyJsonMessage("trappedKeyEvent", data
.Pass());
1062 void ChromotingInstance::SendOutgoingIq(const std::string
& iq
) {
1063 scoped_ptr
<base::DictionaryValue
> data(new base::DictionaryValue());
1064 data
->SetString("iq", iq
);
1065 PostLegacyJsonMessage("sendOutgoingIq", data
.Pass());
1068 void ChromotingInstance::SendPerfStats() {
1069 if (!video_renderer_
.get()) {
1073 plugin_task_runner_
->PostDelayedTask(
1074 FROM_HERE
, base::Bind(&ChromotingInstance::SendPerfStats
,
1075 weak_factory_
.GetWeakPtr()),
1076 base::TimeDelta::FromMilliseconds(kPerfStatsIntervalMs
));
1078 scoped_ptr
<base::DictionaryValue
> data(new base::DictionaryValue());
1079 ChromotingStats
* stats
= video_renderer_
->GetStats();
1080 data
->SetDouble("videoBandwidth", stats
->video_bandwidth()->Rate());
1081 data
->SetDouble("videoFrameRate", stats
->video_frame_rate()->Rate());
1082 data
->SetDouble("captureLatency", stats
->video_capture_ms()->Average());
1083 data
->SetDouble("encodeLatency", stats
->video_encode_ms()->Average());
1084 data
->SetDouble("decodeLatency", stats
->video_decode_ms()->Average());
1085 data
->SetDouble("renderLatency", stats
->video_paint_ms()->Average());
1086 data
->SetDouble("roundtripLatency", stats
->round_trip_ms()->Average());
1087 PostLegacyJsonMessage("onPerfStats", data
.Pass());
1091 void ChromotingInstance::RegisterLogMessageHandler() {
1092 base::AutoLock
lock(g_logging_lock
.Get());
1094 VLOG(1) << "Registering global log handler";
1096 // Record previous handler so we can call it in a chain.
1097 g_logging_old_handler
= logging::GetLogMessageHandler();
1099 // Set up log message handler.
1100 // This is not thread-safe so we need it within our lock.
1101 logging::SetLogMessageHandler(&LogToUI
);
1104 void ChromotingInstance::RegisterLoggingInstance() {
1105 base::AutoLock
lock(g_logging_lock
.Get());
1107 // Register this instance as the one that will handle all logging calls
1108 // and display them to the user.
1109 // If multiple plugins are run, then the last one registered will handle all
1110 // logging for all instances.
1111 g_logging_instance
.Get() = weak_factory_
.GetWeakPtr();
1112 g_logging_task_runner
.Get() = plugin_task_runner_
;
1113 g_has_logging_instance
= true;
1116 void ChromotingInstance::UnregisterLoggingInstance() {
1117 base::AutoLock
lock(g_logging_lock
.Get());
1119 // Don't unregister unless we're the currently registered instance.
1120 if (this != g_logging_instance
.Get().get())
1123 // Unregister this instance for logging.
1124 g_has_logging_instance
= false;
1125 g_logging_instance
.Get().reset();
1126 g_logging_task_runner
.Get() = NULL
;
1128 VLOG(1) << "Unregistering global log handler";
1132 bool ChromotingInstance::LogToUI(int severity
, const char* file
, int line
,
1133 size_t message_start
,
1134 const std::string
& str
) {
1135 // Note that we're reading |g_has_logging_instance| outside of a lock.
1136 // This lockless read is done so that we don't needlessly slow down global
1137 // logging with a lock for each log message.
1139 // This lockless read is safe because:
1141 // Misreading a false value (when it should be true) means that we'll simply
1142 // skip processing a few log messages.
1144 // Misreading a true value (when it should be false) means that we'll take
1145 // the lock and check |g_logging_instance| unnecessarily. This is not
1146 // problematic because we always set |g_logging_instance| inside a lock.
1147 if (g_has_logging_instance
) {
1148 scoped_refptr
<base::SingleThreadTaskRunner
> logging_task_runner
;
1149 base::WeakPtr
<ChromotingInstance
> logging_instance
;
1152 base::AutoLock
lock(g_logging_lock
.Get());
1153 // If we're on the logging thread and |g_logging_to_plugin| is set then
1154 // this LOG message came from handling a previous LOG message and we
1155 // should skip it to avoid an infinite loop of LOG messages.
1156 if (!g_logging_task_runner
.Get()->BelongsToCurrentThread() ||
1157 !g_logging_to_plugin
) {
1158 logging_task_runner
= g_logging_task_runner
.Get();
1159 logging_instance
= g_logging_instance
.Get();
1163 if (logging_task_runner
.get()) {
1164 std::string message
= remoting::GetTimestampString();
1165 message
+= (str
.c_str() + message_start
);
1167 logging_task_runner
->PostTask(
1168 FROM_HERE
, base::Bind(&ChromotingInstance::ProcessLogToUI
,
1169 logging_instance
, message
));
1173 if (g_logging_old_handler
)
1174 return (g_logging_old_handler
)(severity
, file
, line
, message_start
, str
);
1178 void ChromotingInstance::ProcessLogToUI(const std::string
& message
) {
1179 DCHECK(plugin_task_runner_
->BelongsToCurrentThread());
1181 // This flag (which is set only here) is used to prevent LogToUI from posting
1182 // new tasks while we're in the middle of servicing a LOG call. This can
1183 // happen if the call to LogDebugInfo tries to LOG anything.
1184 // Since it is read on the plugin thread, we don't need to lock to set it.
1185 g_logging_to_plugin
= true;
1186 scoped_ptr
<base::DictionaryValue
> data(new base::DictionaryValue());
1187 data
->SetString("message", message
);
1188 PostLegacyJsonMessage("logDebugMessage", data
.Pass());
1189 g_logging_to_plugin
= false;
1192 bool ChromotingInstance::IsCallerAppOrExtension() {
1193 const pp::URLUtil_Dev
* url_util
= pp::URLUtil_Dev::Get();
1197 PP_URLComponents_Dev url_components
;
1198 pp::Var url_var
= url_util
->GetDocumentURL(this, &url_components
);
1199 if (!url_var
.is_string())
1202 std::string url
= url_var
.AsString();
1203 std::string url_scheme
= url
.substr(url_components
.scheme
.begin
,
1204 url_components
.scheme
.len
);
1205 return url_scheme
== kChromeExtensionUrlScheme
;
1208 bool ChromotingInstance::IsConnected() {
1210 (client_
->connection_state() == protocol::ConnectionToHost::CONNECTED
);
1213 void ChromotingInstance::OnMediaSourceSize(const webrtc::DesktopSize
& size
,
1214 const webrtc::DesktopVector
& dpi
) {
1215 SetDesktopSize(size
, dpi
);
1218 void ChromotingInstance::OnMediaSourceShape(
1219 const webrtc::DesktopRegion
& shape
) {
1220 SetDesktopShape(shape
);
1223 void ChromotingInstance::OnMediaSourceReset(const std::string
& format
) {
1224 scoped_ptr
<base::DictionaryValue
> data(new base::DictionaryValue());
1225 data
->SetString("format", format
);
1226 PostLegacyJsonMessage("mediaSourceReset", data
.Pass());
1229 void ChromotingInstance::OnMediaSourceData(uint8_t* buffer
, size_t buffer_size
,
1231 pp::VarArrayBuffer
array_buffer(buffer_size
);
1232 void* data_ptr
= array_buffer
.Map();
1233 memcpy(data_ptr
, buffer
, buffer_size
);
1234 array_buffer
.Unmap();
1235 pp::VarDictionary data_dictionary
;
1236 data_dictionary
.Set(pp::Var("buffer"), array_buffer
);
1237 data_dictionary
.Set(pp::Var("keyframe"), keyframe
);
1238 PostChromotingMessage("mediaSourceData", data_dictionary
);
1241 } // namespace remoting