OLD | NEW |
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 #include "remoting/host/chromoting_host.h" | 5 #include "remoting/host/chromoting_host.h" |
6 | 6 |
7 #include "base/bind.h" | 7 #include "base/bind.h" |
8 #include "base/callback.h" | 8 #include "base/callback.h" |
9 #include "base/logging.h" | 9 #include "base/logging.h" |
10 #include "base/message_loop_proxy.h" | 10 #include "base/message_loop_proxy.h" |
11 #include "build/build_config.h" | 11 #include "build/build_config.h" |
12 #include "remoting/base/constants.h" | 12 #include "remoting/base/constants.h" |
13 #include "remoting/codec/audio_encoder.h" | 13 #include "remoting/codec/audio_encoder.h" |
14 #include "remoting/codec/audio_encoder_speex.h" | 14 #include "remoting/codec/audio_encoder_speex.h" |
15 #include "remoting/codec/audio_encoder_verbatim.h" | 15 #include "remoting/codec/audio_encoder_verbatim.h" |
16 #include "remoting/codec/video_encoder.h" | 16 #include "remoting/codec/video_encoder.h" |
17 #include "remoting/codec/video_encoder_row_based.h" | 17 #include "remoting/codec/video_encoder_row_based.h" |
18 #include "remoting/codec/video_encoder_vp8.h" | 18 #include "remoting/codec/video_encoder_vp8.h" |
19 #include "remoting/host/audio_scheduler.h" | 19 #include "remoting/host/audio_scheduler.h" |
20 #include "remoting/host/chromoting_host_context.h" | 20 #include "remoting/host/chromoting_host_context.h" |
21 #include "remoting/host/desktop_environment.h" | 21 #include "remoting/host/desktop_environment.h" |
| 22 #include "remoting/host/desktop_environment_factory.h" |
22 #include "remoting/host/event_executor.h" | 23 #include "remoting/host/event_executor.h" |
23 #include "remoting/host/host_config.h" | 24 #include "remoting/host/host_config.h" |
24 #include "remoting/host/screen_recorder.h" | |
25 #include "remoting/protocol/connection_to_client.h" | 25 #include "remoting/protocol/connection_to_client.h" |
26 #include "remoting/protocol/client_stub.h" | 26 #include "remoting/protocol/client_stub.h" |
27 #include "remoting/protocol/host_stub.h" | 27 #include "remoting/protocol/host_stub.h" |
28 #include "remoting/protocol/input_stub.h" | 28 #include "remoting/protocol/input_stub.h" |
29 #include "remoting/protocol/session_config.h" | 29 #include "remoting/protocol/session_config.h" |
30 | 30 |
31 using remoting::protocol::ConnectionToClient; | 31 using remoting::protocol::ConnectionToClient; |
32 using remoting::protocol::InputStub; | 32 using remoting::protocol::InputStub; |
33 | 33 |
34 namespace remoting { | 34 namespace remoting { |
(...skipping 24 matching lines...) Expand all Loading... |
59 | 59 |
60 // Don't use initial delay unless the last request was an error. | 60 // Don't use initial delay unless the last request was an error. |
61 false, | 61 false, |
62 }; | 62 }; |
63 | 63 |
64 } // namespace | 64 } // namespace |
65 | 65 |
66 ChromotingHost::ChromotingHost( | 66 ChromotingHost::ChromotingHost( |
67 ChromotingHostContext* context, | 67 ChromotingHostContext* context, |
68 SignalStrategy* signal_strategy, | 68 SignalStrategy* signal_strategy, |
69 DesktopEnvironment* environment, | 69 DesktopEnvironmentFactory* desktop_environment_factory, |
70 scoped_ptr<protocol::SessionManager> session_manager) | 70 scoped_ptr<protocol::SessionManager> session_manager) |
71 : context_(context), | 71 : context_(context), |
72 desktop_environment_(environment), | 72 desktop_environment_factory_(desktop_environment_factory), |
73 session_manager_(session_manager.Pass()), | 73 session_manager_(session_manager.Pass()), |
74 signal_strategy_(signal_strategy), | 74 signal_strategy_(signal_strategy), |
75 stopping_recorders_(0), | 75 clients_count_(0), |
76 state_(kInitial), | 76 state_(kInitial), |
77 protocol_config_(protocol::CandidateSessionConfig::CreateDefault()), | 77 protocol_config_(protocol::CandidateSessionConfig::CreateDefault()), |
78 login_backoff_(&kDefaultBackoffPolicy), | 78 login_backoff_(&kDefaultBackoffPolicy), |
79 authenticating_client_(false), | 79 authenticating_client_(false), |
80 reject_authenticating_client_(false) { | 80 reject_authenticating_client_(false) { |
81 DCHECK(context_); | 81 DCHECK(context_); |
82 DCHECK(signal_strategy); | 82 DCHECK(signal_strategy); |
83 DCHECK(desktop_environment_); | |
84 DCHECK(context_->network_task_runner()->BelongsToCurrentThread()); | 83 DCHECK(context_->network_task_runner()->BelongsToCurrentThread()); |
85 | 84 |
86 if (!desktop_environment_->audio_capturer()) { | 85 if (!desktop_environment_factory_->SupportsAudioCapture()) { |
87 // Disable audio by replacing our list of supported audio configurations | 86 // Disable audio by replacing our list of supported audio configurations |
88 // with the NONE config. | 87 // with the NONE config. |
89 protocol_config_->mutable_audio_configs()->clear(); | 88 protocol_config_->mutable_audio_configs()->clear(); |
90 protocol_config_->mutable_audio_configs()->push_back( | 89 protocol_config_->mutable_audio_configs()->push_back( |
91 protocol::ChannelConfig()); | 90 protocol::ChannelConfig()); |
92 } | 91 } |
93 } | 92 } |
94 | 93 |
95 ChromotingHost::~ChromotingHost() { | 94 ChromotingHost::~ChromotingHost() { |
96 DCHECK(clients_.empty()); | 95 DCHECK(clients_.empty()); |
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
134 // We are already stopping. Just save the task. | 133 // We are already stopping. Just save the task. |
135 if (!shutdown_task.is_null()) | 134 if (!shutdown_task.is_null()) |
136 shutdown_tasks_.push_back(shutdown_task); | 135 shutdown_tasks_.push_back(shutdown_task); |
137 break; | 136 break; |
138 | 137 |
139 case kStarted: | 138 case kStarted: |
140 if (!shutdown_task.is_null()) | 139 if (!shutdown_task.is_null()) |
141 shutdown_tasks_.push_back(shutdown_task); | 140 shutdown_tasks_.push_back(shutdown_task); |
142 state_ = kStopping; | 141 state_ = kStopping; |
143 | 142 |
144 // Disconnect all of the clients, implicitly stopping the ScreenRecorder. | 143 // Disconnect all of the clients. |
145 while (!clients_.empty()) { | 144 while (!clients_.empty()) { |
146 clients_.front()->Disconnect(); | 145 clients_.front()->Disconnect(); |
147 } | 146 } |
148 DCHECK(!recorder_.get()); | |
149 DCHECK(!audio_scheduler_.get()); | |
150 | 147 |
151 // Destroy session manager. | 148 // Run the remaining shutdown tasks. |
152 session_manager_.reset(); | 149 if (state_ == kStopping && !clients_count_) |
| 150 ShutdownFinish(); |
153 | 151 |
154 if (!stopping_recorders_) | |
155 ShutdownFinish(); | |
156 break; | 152 break; |
157 } | 153 } |
158 } | 154 } |
159 | 155 |
160 void ChromotingHost::AddStatusObserver(HostStatusObserver* observer) { | 156 void ChromotingHost::AddStatusObserver(HostStatusObserver* observer) { |
161 DCHECK(context_->network_task_runner()->BelongsToCurrentThread()); | 157 DCHECK(context_->network_task_runner()->BelongsToCurrentThread()); |
162 status_observers_.AddObserver(observer); | 158 status_observers_.AddObserver(observer); |
163 } | 159 } |
164 | 160 |
165 void ChromotingHost::RemoveStatusObserver(HostStatusObserver* observer) { | 161 void ChromotingHost::RemoveStatusObserver(HostStatusObserver* observer) { |
(...skipping 30 matching lines...) Expand all Loading... |
196 ClientList clients_copy(clients_); | 192 ClientList clients_copy(clients_); |
197 for (ClientList::const_iterator other_client = clients_copy.begin(); | 193 for (ClientList::const_iterator other_client = clients_copy.begin(); |
198 other_client != clients_copy.end(); ++other_client) { | 194 other_client != clients_copy.end(); ++other_client) { |
199 if ((*other_client) != client) { | 195 if ((*other_client) != client) { |
200 (*other_client)->Disconnect(); | 196 (*other_client)->Disconnect(); |
201 } | 197 } |
202 } | 198 } |
203 | 199 |
204 // Disconnects above must have destroyed all other clients and |recorder_|. | 200 // Disconnects above must have destroyed all other clients and |recorder_|. |
205 DCHECK_EQ(clients_.size(), 1U); | 201 DCHECK_EQ(clients_.size(), 1U); |
206 DCHECK(!recorder_.get()); | |
207 DCHECK(!audio_scheduler_.get()); | |
208 | 202 |
209 // Notify observers that there is at least one authenticated client. | 203 // Notify observers that there is at least one authenticated client. |
210 const std::string& jid = client->client_jid(); | 204 const std::string& jid = client->client_jid(); |
211 | 205 |
212 reject_authenticating_client_ = false; | 206 reject_authenticating_client_ = false; |
213 | 207 |
214 authenticating_client_ = true; | 208 authenticating_client_ = true; |
215 FOR_EACH_OBSERVER(HostStatusObserver, status_observers_, | 209 FOR_EACH_OBSERVER(HostStatusObserver, status_observers_, |
216 OnClientAuthenticated(jid)); | 210 OnClientAuthenticated(jid)); |
217 authenticating_client_ = false; | 211 authenticating_client_ = false; |
218 | 212 |
219 if (reject_authenticating_client_) { | 213 if (reject_authenticating_client_) { |
220 client->Disconnect(); | 214 client->Disconnect(); |
221 } | 215 } |
222 } | 216 } |
223 | 217 |
224 void ChromotingHost::OnSessionChannelsConnected(ClientSession* client) { | 218 void ChromotingHost::OnSessionChannelsConnected(ClientSession* client) { |
225 DCHECK(context_->network_task_runner()->BelongsToCurrentThread()); | 219 DCHECK(context_->network_task_runner()->BelongsToCurrentThread()); |
226 | 220 |
227 // Then we create a ScreenRecorder passing the message loops that | 221 // Notify observers. |
228 // it should run on. | |
229 VideoEncoder* video_encoder = | |
230 CreateVideoEncoder(client->connection()->session()->config()); | |
231 | |
232 recorder_ = new ScreenRecorder(context_->capture_task_runner(), | |
233 context_->encode_task_runner(), | |
234 context_->network_task_runner(), | |
235 desktop_environment_->capturer(), | |
236 video_encoder); | |
237 if (client->connection()->session()->config().is_audio_enabled()) { | |
238 scoped_ptr<AudioEncoder> audio_encoder = | |
239 CreateAudioEncoder(client->connection()->session()->config()); | |
240 audio_scheduler_ = new AudioScheduler( | |
241 context_->audio_task_runner(), | |
242 context_->network_task_runner(), | |
243 desktop_environment_->audio_capturer(), | |
244 audio_encoder.Pass(), | |
245 client->connection()->audio_stub()); | |
246 } | |
247 | |
248 // Immediately add the connection and start the session. | |
249 recorder_->AddConnection(client->connection()); | |
250 recorder_->Start(); | |
251 desktop_environment_->OnSessionStarted(client->CreateClipboardProxy()); | |
252 | |
253 FOR_EACH_OBSERVER(HostStatusObserver, status_observers_, | 222 FOR_EACH_OBSERVER(HostStatusObserver, status_observers_, |
254 OnClientConnected(client->client_jid())); | 223 OnClientConnected(client->client_jid())); |
255 } | 224 } |
256 | 225 |
257 void ChromotingHost::OnSessionAuthenticationFailed(ClientSession* client) { | 226 void ChromotingHost::OnSessionAuthenticationFailed(ClientSession* client) { |
258 DCHECK(context_->network_task_runner()->BelongsToCurrentThread()); | 227 DCHECK(context_->network_task_runner()->BelongsToCurrentThread()); |
259 | 228 |
260 // Notify observers. | 229 // Notify observers. |
261 FOR_EACH_OBSERVER(HostStatusObserver, status_observers_, | 230 FOR_EACH_OBSERVER(HostStatusObserver, status_observers_, |
262 OnAccessDenied(client->client_jid())); | 231 OnAccessDenied(client->client_jid())); |
263 } | 232 } |
264 | 233 |
265 void ChromotingHost::OnSessionClosed(ClientSession* client) { | 234 void ChromotingHost::OnSessionClosed(ClientSession* client) { |
266 DCHECK(context_->network_task_runner()->BelongsToCurrentThread()); | 235 DCHECK(context_->network_task_runner()->BelongsToCurrentThread()); |
267 | 236 |
268 scoped_ptr<ClientSession> client_destroyer(client); | |
269 | |
270 ClientList::iterator it = std::find(clients_.begin(), clients_.end(), client); | 237 ClientList::iterator it = std::find(clients_.begin(), clients_.end(), client); |
271 CHECK(it != clients_.end()); | 238 CHECK(it != clients_.end()); |
272 clients_.erase(it); | 239 clients_.erase(it); |
273 | 240 |
274 if (recorder_.get()) { | |
275 recorder_->RemoveConnection(client->connection()); | |
276 } | |
277 | |
278 if (audio_scheduler_.get()) { | |
279 audio_scheduler_->OnClientDisconnected(); | |
280 StopAudioScheduler(); | |
281 } | |
282 | |
283 if (client->is_authenticated()) { | 241 if (client->is_authenticated()) { |
284 FOR_EACH_OBSERVER(HostStatusObserver, status_observers_, | 242 FOR_EACH_OBSERVER(HostStatusObserver, status_observers_, |
285 OnClientDisconnected(client->client_jid())); | 243 OnClientDisconnected(client->client_jid())); |
| 244 } |
286 | 245 |
287 // TODO(sergeyu): This teardown logic belongs to ClientSession | 246 client->StopAndDelete(base::Bind(&ChromotingHost::OnClientStopped, this)); |
288 // class. It should start/stop screen recorder or tell the host | |
289 // when to do it. | |
290 if (recorder_.get()) { | |
291 // Currently we don't allow more than one simultaneous connection, | |
292 // so we need to shutdown recorder when a client disconnects. | |
293 StopScreenRecorder(); | |
294 } | |
295 desktop_environment_->OnSessionFinished(); | |
296 } | |
297 } | 247 } |
298 | 248 |
299 void ChromotingHost::OnSessionSequenceNumber(ClientSession* session, | 249 void ChromotingHost::OnSessionSequenceNumber(ClientSession* session, |
300 int64 sequence_number) { | 250 int64 sequence_number) { |
301 DCHECK(context_->network_task_runner()->BelongsToCurrentThread()); | 251 DCHECK(context_->network_task_runner()->BelongsToCurrentThread()); |
302 if (recorder_.get()) | |
303 recorder_->UpdateSequenceNumber(sequence_number); | |
304 } | 252 } |
305 | 253 |
306 void ChromotingHost::OnSessionRouteChange( | 254 void ChromotingHost::OnSessionRouteChange( |
307 ClientSession* session, | 255 ClientSession* session, |
308 const std::string& channel_name, | 256 const std::string& channel_name, |
309 const protocol::TransportRoute& route) { | 257 const protocol::TransportRoute& route) { |
310 DCHECK(context_->network_task_runner()->BelongsToCurrentThread()); | 258 DCHECK(context_->network_task_runner()->BelongsToCurrentThread()); |
311 FOR_EACH_OBSERVER(HostStatusObserver, status_observers_, | 259 FOR_EACH_OBSERVER(HostStatusObserver, status_observers_, |
312 OnClientRouteChange(session->client_jid(), channel_name, | 260 OnClientRouteChange(session->client_jid(), channel_name, |
313 route)); | 261 route)); |
(...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
347 *response = protocol::SessionManager::INCOMPATIBLE; | 295 *response = protocol::SessionManager::INCOMPATIBLE; |
348 return; | 296 return; |
349 } | 297 } |
350 | 298 |
351 session->set_config(config); | 299 session->set_config(config); |
352 | 300 |
353 *response = protocol::SessionManager::ACCEPT; | 301 *response = protocol::SessionManager::ACCEPT; |
354 | 302 |
355 LOG(INFO) << "Client connected: " << session->jid(); | 303 LOG(INFO) << "Client connected: " << session->jid(); |
356 | 304 |
| 305 // Create the desktop integration implementation for the client to use. |
| 306 scoped_ptr<DesktopEnvironment> desktop_environment = |
| 307 desktop_environment_factory_->Create(context_); |
| 308 |
357 // Create a client object. | 309 // Create a client object. |
358 scoped_ptr<protocol::ConnectionToClient> connection( | 310 scoped_ptr<protocol::ConnectionToClient> connection( |
359 new protocol::ConnectionToClient(session)); | 311 new protocol::ConnectionToClient(session)); |
360 ClientSession* client = new ClientSession( | 312 ClientSession* client = new ClientSession( |
361 this, | 313 this, |
| 314 context_->capture_task_runner(), |
| 315 context_->encode_task_runner(), |
| 316 context_->network_task_runner(), |
362 connection.Pass(), | 317 connection.Pass(), |
363 desktop_environment_->event_executor(), | 318 desktop_environment.Pass(), |
364 desktop_environment_->event_executor(), | |
365 desktop_environment_->capturer(), | |
366 max_session_duration_); | 319 max_session_duration_); |
367 clients_.push_back(client); | 320 clients_.push_back(client); |
| 321 ++clients_count_; |
368 } | 322 } |
369 | 323 |
370 void ChromotingHost::set_protocol_config( | 324 void ChromotingHost::set_protocol_config( |
371 protocol::CandidateSessionConfig* config) { | 325 protocol::CandidateSessionConfig* config) { |
372 DCHECK(context_->network_task_runner()->BelongsToCurrentThread()); | 326 DCHECK(context_->network_task_runner()->BelongsToCurrentThread()); |
373 DCHECK(config); | 327 DCHECK(config); |
374 DCHECK_EQ(state_, kInitial); | 328 DCHECK_EQ(state_, kInitial); |
375 protocol_config_.reset(config); | 329 protocol_config_.reset(config); |
376 } | 330 } |
377 | 331 |
(...skipping 38 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
416 } | 370 } |
417 } | 371 } |
418 | 372 |
419 void ChromotingHost::SetUiStrings(const UiStrings& ui_strings) { | 373 void ChromotingHost::SetUiStrings(const UiStrings& ui_strings) { |
420 DCHECK(context_->network_task_runner()->BelongsToCurrentThread()); | 374 DCHECK(context_->network_task_runner()->BelongsToCurrentThread()); |
421 DCHECK_EQ(state_, kInitial); | 375 DCHECK_EQ(state_, kInitial); |
422 | 376 |
423 ui_strings_ = ui_strings; | 377 ui_strings_ = ui_strings; |
424 } | 378 } |
425 | 379 |
426 // TODO(sergeyu): Move this to SessionManager? | 380 void ChromotingHost::OnClientStopped() { |
427 // static | 381 DCHECK(context_->network_task_runner()->BelongsToCurrentThread()); |
428 VideoEncoder* ChromotingHost::CreateVideoEncoder( | |
429 const protocol::SessionConfig& config) { | |
430 const protocol::ChannelConfig& video_config = config.video_config(); | |
431 | 382 |
432 if (video_config.codec == protocol::ChannelConfig::CODEC_VERBATIM) { | 383 --clients_count_; |
433 return VideoEncoderRowBased::CreateVerbatimEncoder(); | 384 if (state_ == kStopping && !clients_count_) |
434 } else if (video_config.codec == protocol::ChannelConfig::CODEC_ZIP) { | |
435 return VideoEncoderRowBased::CreateZlibEncoder(); | |
436 } else if (video_config.codec == protocol::ChannelConfig::CODEC_VP8) { | |
437 return new remoting::VideoEncoderVp8(); | |
438 } | |
439 | |
440 return NULL; | |
441 } | |
442 | |
443 // static | |
444 scoped_ptr<AudioEncoder> ChromotingHost::CreateAudioEncoder( | |
445 const protocol::SessionConfig& config) { | |
446 const protocol::ChannelConfig& audio_config = config.audio_config(); | |
447 | |
448 if (audio_config.codec == protocol::ChannelConfig::CODEC_VERBATIM) { | |
449 return scoped_ptr<AudioEncoder>(new AudioEncoderVerbatim()); | |
450 } else if (audio_config.codec == protocol::ChannelConfig::CODEC_SPEEX) { | |
451 return scoped_ptr<AudioEncoder>(new AudioEncoderSpeex()); | |
452 } | |
453 | |
454 NOTIMPLEMENTED(); | |
455 return scoped_ptr<AudioEncoder>(NULL); | |
456 } | |
457 | |
458 void ChromotingHost::StopScreenRecorder() { | |
459 DCHECK(context_->network_task_runner()->BelongsToCurrentThread()); | |
460 DCHECK(recorder_.get()); | |
461 | |
462 ++stopping_recorders_; | |
463 scoped_refptr<ScreenRecorder> recorder = recorder_; | |
464 recorder_ = NULL; | |
465 recorder->Stop(base::Bind(&ChromotingHost::OnRecorderStopped, this)); | |
466 } | |
467 | |
468 void ChromotingHost::StopAudioScheduler() { | |
469 DCHECK(context_->network_task_runner()->BelongsToCurrentThread()); | |
470 DCHECK(audio_scheduler_.get()); | |
471 | |
472 ++stopping_recorders_; | |
473 scoped_refptr<AudioScheduler> recorder = audio_scheduler_; | |
474 audio_scheduler_ = NULL; | |
475 recorder->Stop(base::Bind(&ChromotingHost::OnRecorderStopped, this)); | |
476 } | |
477 | |
478 void ChromotingHost::OnRecorderStopped() { | |
479 if (!context_->network_task_runner()->BelongsToCurrentThread()) { | |
480 context_->network_task_runner()->PostTask( | |
481 FROM_HERE, base::Bind(&ChromotingHost::OnRecorderStopped, this)); | |
482 return; | |
483 } | |
484 | |
485 --stopping_recorders_; | |
486 DCHECK_GE(stopping_recorders_, 0); | |
487 | |
488 if (!stopping_recorders_ && state_ == kStopping) | |
489 ShutdownFinish(); | 385 ShutdownFinish(); |
490 } | 386 } |
491 | 387 |
492 void ChromotingHost::ShutdownFinish() { | 388 void ChromotingHost::ShutdownFinish() { |
493 DCHECK(context_->network_task_runner()->BelongsToCurrentThread()); | 389 DCHECK(context_->network_task_runner()->BelongsToCurrentThread()); |
494 DCHECK(!stopping_recorders_); | 390 DCHECK_EQ(state_, kStopping); |
| 391 |
| 392 // Destroy session manager. |
| 393 session_manager_.reset(); |
495 | 394 |
496 state_ = kStopped; | 395 state_ = kStopped; |
497 | 396 |
498 // Keep reference to |this|, so that we don't get destroyed while | 397 // Keep reference to |this|, so that we don't get destroyed while |
499 // sending notifications. | 398 // sending notifications. |
500 scoped_refptr<ChromotingHost> self(this); | 399 scoped_refptr<ChromotingHost> self(this); |
501 | 400 |
502 // Notify observers. | 401 // Notify observers. |
503 FOR_EACH_OBSERVER(HostStatusObserver, status_observers_, | 402 FOR_EACH_OBSERVER(HostStatusObserver, status_observers_, |
504 OnShutdown()); | 403 OnShutdown()); |
505 | 404 |
506 for (std::vector<base::Closure>::iterator it = shutdown_tasks_.begin(); | 405 for (std::vector<base::Closure>::iterator it = shutdown_tasks_.begin(); |
507 it != shutdown_tasks_.end(); ++it) { | 406 it != shutdown_tasks_.end(); ++it) { |
508 it->Run(); | 407 it->Run(); |
509 } | 408 } |
510 shutdown_tasks_.clear(); | 409 shutdown_tasks_.clear(); |
511 } | 410 } |
512 | 411 |
513 } // namespace remoting | 412 } // namespace remoting |
OLD | NEW |