| 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/client_session.h" | 5 #include "remoting/host/client_session.h" |
| 6 | 6 |
| 7 #include <algorithm> | 7 #include <algorithm> |
| 8 | 8 |
| 9 #include "base/message_loop/message_loop_proxy.h" | 9 #include "base/message_loop/message_loop_proxy.h" |
| 10 #include "remoting/base/capabilities.h" | 10 #include "remoting/base/capabilities.h" |
| 11 #include "remoting/base/logging.h" | 11 #include "remoting/base/logging.h" |
| 12 #include "remoting/codec/audio_encoder.h" | 12 #include "remoting/codec/audio_encoder.h" |
| 13 #include "remoting/codec/audio_encoder_opus.h" | 13 #include "remoting/codec/audio_encoder_opus.h" |
| 14 #include "remoting/codec/audio_encoder_verbatim.h" | 14 #include "remoting/codec/audio_encoder_verbatim.h" |
| 15 #include "remoting/codec/video_encoder.h" | 15 #include "remoting/codec/video_encoder.h" |
| 16 #include "remoting/codec/video_encoder_verbatim.h" | 16 #include "remoting/codec/video_encoder_verbatim.h" |
| 17 #include "remoting/codec/video_encoder_vpx.h" | 17 #include "remoting/codec/video_encoder_vpx.h" |
| 18 #include "remoting/host/audio_capturer.h" | 18 #include "remoting/host/audio_capturer.h" |
| 19 #include "remoting/host/audio_scheduler.h" | 19 #include "remoting/host/audio_pump.h" |
| 20 #include "remoting/host/desktop_environment.h" | 20 #include "remoting/host/desktop_environment.h" |
| 21 #include "remoting/host/host_extension_session.h" | 21 #include "remoting/host/host_extension_session.h" |
| 22 #include "remoting/host/input_injector.h" | 22 #include "remoting/host/input_injector.h" |
| 23 #include "remoting/host/mouse_shape_pump.h" | 23 #include "remoting/host/mouse_shape_pump.h" |
| 24 #include "remoting/host/screen_capturer_proxy.h" | 24 #include "remoting/host/screen_capturer_proxy.h" |
| 25 #include "remoting/host/screen_controls.h" | 25 #include "remoting/host/screen_controls.h" |
| 26 #include "remoting/host/screen_resolution.h" | 26 #include "remoting/host/screen_resolution.h" |
| 27 #include "remoting/host/video_frame_pump.h" | 27 #include "remoting/host/video_frame_pump.h" |
| 28 #include "remoting/proto/control.pb.h" | 28 #include "remoting/proto/control.pb.h" |
| 29 #include "remoting/proto/event.pb.h" | 29 #include "remoting/proto/event.pb.h" |
| 30 #include "remoting/protocol/client_stub.h" | 30 #include "remoting/protocol/client_stub.h" |
| 31 #include "remoting/protocol/clipboard_thread_proxy.h" | 31 #include "remoting/protocol/clipboard_thread_proxy.h" |
| 32 #include "remoting/protocol/pairing_registry.h" | 32 #include "remoting/protocol/pairing_registry.h" |
| 33 #include "third_party/webrtc/modules/desktop_capture/desktop_capturer.h" | 33 #include "third_party/webrtc/modules/desktop_capture/desktop_capturer.h" |
| 34 #include "third_party/webrtc/modules/desktop_capture/mouse_cursor_monitor.h" | 34 #include "third_party/webrtc/modules/desktop_capture/mouse_cursor_monitor.h" |
| 35 | 35 |
| 36 // Default DPI to assume for old clients that use notifyClientDimensions. | 36 // Default DPI to assume for old clients that use notifyClientDimensions. |
| 37 const int kDefaultDPI = 96; | 37 const int kDefaultDPI = 96; |
| 38 | 38 |
| 39 namespace remoting { | 39 namespace remoting { |
| 40 | 40 |
| 41 namespace { |
| 42 |
| 43 scoped_ptr<VideoEncoder> CreateVideoEncoder( |
| 44 const protocol::SessionConfig& config) { |
| 45 const protocol::ChannelConfig& video_config = config.video_config(); |
| 46 |
| 47 if (video_config.codec == protocol::ChannelConfig::CODEC_VP8) { |
| 48 return VideoEncoderVpx::CreateForVP8().Pass(); |
| 49 } else if (video_config.codec == protocol::ChannelConfig::CODEC_VP9) { |
| 50 return VideoEncoderVpx::CreateForVP9().Pass(); |
| 51 } else if (video_config.codec == protocol::ChannelConfig::CODEC_VERBATIM) { |
| 52 return make_scoped_ptr(new VideoEncoderVerbatim()); |
| 53 } |
| 54 |
| 55 NOTREACHED(); |
| 56 return nullptr; |
| 57 } |
| 58 |
| 59 scoped_ptr<AudioEncoder> CreateAudioEncoder( |
| 60 const protocol::SessionConfig& config) { |
| 61 const protocol::ChannelConfig& audio_config = config.audio_config(); |
| 62 |
| 63 if (audio_config.codec == protocol::ChannelConfig::CODEC_VERBATIM) { |
| 64 return make_scoped_ptr(new AudioEncoderVerbatim()); |
| 65 } else if (audio_config.codec == protocol::ChannelConfig::CODEC_OPUS) { |
| 66 return make_scoped_ptr(new AudioEncoderOpus()); |
| 67 } |
| 68 |
| 69 NOTREACHED(); |
| 70 return nullptr; |
| 71 } |
| 72 |
| 73 } // namespace |
| 74 |
| 41 ClientSession::ClientSession( | 75 ClientSession::ClientSession( |
| 42 EventHandler* event_handler, | 76 EventHandler* event_handler, |
| 43 scoped_refptr<base::SingleThreadTaskRunner> audio_task_runner, | 77 scoped_refptr<base::SingleThreadTaskRunner> audio_task_runner, |
| 44 scoped_refptr<base::SingleThreadTaskRunner> input_task_runner, | 78 scoped_refptr<base::SingleThreadTaskRunner> input_task_runner, |
| 45 scoped_refptr<base::SingleThreadTaskRunner> video_capture_task_runner, | 79 scoped_refptr<base::SingleThreadTaskRunner> video_capture_task_runner, |
| 46 scoped_refptr<base::SingleThreadTaskRunner> video_encode_task_runner, | 80 scoped_refptr<base::SingleThreadTaskRunner> video_encode_task_runner, |
| 47 scoped_refptr<base::SingleThreadTaskRunner> network_task_runner, | 81 scoped_refptr<base::SingleThreadTaskRunner> network_task_runner, |
| 48 scoped_refptr<base::SingleThreadTaskRunner> ui_task_runner, | 82 scoped_refptr<base::SingleThreadTaskRunner> ui_task_runner, |
| 49 scoped_ptr<protocol::ConnectionToClient> connection, | 83 scoped_ptr<protocol::ConnectionToClient> connection, |
| 50 DesktopEnvironmentFactory* desktop_environment_factory, | 84 DesktopEnvironmentFactory* desktop_environment_factory, |
| (...skipping 42 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 93 | 127 |
| 94 #if defined(OS_WIN) | 128 #if defined(OS_WIN) |
| 95 // LocalInputMonitorWin filters out an echo of the injected input before it | 129 // LocalInputMonitorWin filters out an echo of the injected input before it |
| 96 // reaches |remote_input_filter_|. | 130 // reaches |remote_input_filter_|. |
| 97 remote_input_filter_.SetExpectLocalEcho(false); | 131 remote_input_filter_.SetExpectLocalEcho(false); |
| 98 #endif // defined(OS_WIN) | 132 #endif // defined(OS_WIN) |
| 99 } | 133 } |
| 100 | 134 |
| 101 ClientSession::~ClientSession() { | 135 ClientSession::~ClientSession() { |
| 102 DCHECK(CalledOnValidThread()); | 136 DCHECK(CalledOnValidThread()); |
| 103 DCHECK(!audio_scheduler_.get()); | 137 DCHECK(!audio_pump_); |
| 104 DCHECK(!desktop_environment_); | 138 DCHECK(!desktop_environment_); |
| 105 DCHECK(!input_injector_); | 139 DCHECK(!input_injector_); |
| 106 DCHECK(!screen_controls_); | 140 DCHECK(!screen_controls_); |
| 107 DCHECK(!video_frame_pump_.get()); | 141 DCHECK(!video_frame_pump_); |
| 108 | 142 |
| 109 connection_.reset(); | 143 connection_.reset(); |
| 110 } | 144 } |
| 111 | 145 |
| 112 void ClientSession::NotifyClientResolution( | 146 void ClientSession::NotifyClientResolution( |
| 113 const protocol::ClientResolution& resolution) { | 147 const protocol::ClientResolution& resolution) { |
| 114 DCHECK(CalledOnValidThread()); | 148 DCHECK(CalledOnValidThread()); |
| 115 | 149 |
| 116 // TODO(sergeyu): Move these checks to protocol layer. | 150 // TODO(sergeyu): Move these checks to protocol layer. |
| 117 if (!resolution.has_dips_width() || !resolution.has_dips_height() || | 151 if (!resolution.has_dips_width() || !resolution.has_dips_height() || |
| (...skipping 45 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 163 video_frame_pump_->SetLosslessColor(lossless_video_color_); | 197 video_frame_pump_->SetLosslessColor(lossless_video_color_); |
| 164 } | 198 } |
| 165 } | 199 } |
| 166 | 200 |
| 167 void ClientSession::ControlAudio(const protocol::AudioControl& audio_control) { | 201 void ClientSession::ControlAudio(const protocol::AudioControl& audio_control) { |
| 168 DCHECK(CalledOnValidThread()); | 202 DCHECK(CalledOnValidThread()); |
| 169 | 203 |
| 170 if (audio_control.has_enable()) { | 204 if (audio_control.has_enable()) { |
| 171 VLOG(1) << "Received AudioControl (enable=" | 205 VLOG(1) << "Received AudioControl (enable=" |
| 172 << audio_control.enable() << ")"; | 206 << audio_control.enable() << ")"; |
| 173 if (audio_scheduler_.get()) | 207 if (audio_pump_) |
| 174 audio_scheduler_->Pause(!audio_control.enable()); | 208 audio_pump_->Pause(!audio_control.enable()); |
| 175 } | 209 } |
| 176 } | 210 } |
| 177 | 211 |
| 178 void ClientSession::SetCapabilities( | 212 void ClientSession::SetCapabilities( |
| 179 const protocol::Capabilities& capabilities) { | 213 const protocol::Capabilities& capabilities) { |
| 180 DCHECK(CalledOnValidThread()); | 214 DCHECK(CalledOnValidThread()); |
| 181 | 215 |
| 182 // Ignore all the messages but the 1st one. | 216 // Ignore all the messages but the 1st one. |
| 183 if (client_capabilities_) { | 217 if (client_capabilities_) { |
| 184 LOG(WARNING) << "protocol::Capabilities has been received already."; | 218 LOG(WARNING) << "protocol::Capabilities has been received already."; |
| (...skipping 57 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 242 | 276 |
| 243 void ClientSession::OnConnectionAuthenticating( | 277 void ClientSession::OnConnectionAuthenticating( |
| 244 protocol::ConnectionToClient* connection) { | 278 protocol::ConnectionToClient* connection) { |
| 245 event_handler_->OnSessionAuthenticating(this); | 279 event_handler_->OnSessionAuthenticating(this); |
| 246 } | 280 } |
| 247 | 281 |
| 248 void ClientSession::OnConnectionAuthenticated( | 282 void ClientSession::OnConnectionAuthenticated( |
| 249 protocol::ConnectionToClient* connection) { | 283 protocol::ConnectionToClient* connection) { |
| 250 DCHECK(CalledOnValidThread()); | 284 DCHECK(CalledOnValidThread()); |
| 251 DCHECK_EQ(connection_.get(), connection); | 285 DCHECK_EQ(connection_.get(), connection); |
| 252 DCHECK(!audio_scheduler_.get()); | 286 DCHECK(!audio_pump_); |
| 253 DCHECK(!desktop_environment_); | 287 DCHECK(!desktop_environment_); |
| 254 DCHECK(!input_injector_); | 288 DCHECK(!input_injector_); |
| 255 DCHECK(!screen_controls_); | 289 DCHECK(!screen_controls_); |
| 256 DCHECK(!video_frame_pump_.get()); | 290 DCHECK(!video_frame_pump_); |
| 257 | 291 |
| 258 auth_input_filter_.set_enabled(true); | 292 auth_input_filter_.set_enabled(true); |
| 259 auth_clipboard_filter_.set_enabled(true); | 293 auth_clipboard_filter_.set_enabled(true); |
| 260 | 294 |
| 261 clipboard_echo_filter_.set_client_stub(connection_->client_stub()); | 295 clipboard_echo_filter_.set_client_stub(connection_->client_stub()); |
| 262 mouse_clamping_filter_.set_video_stub(connection_->video_stub()); | 296 mouse_clamping_filter_.set_video_stub(connection_->video_stub()); |
| 263 | 297 |
| 264 if (max_duration_ > base::TimeDelta()) { | 298 if (max_duration_ > base::TimeDelta()) { |
| 265 // TODO(simonmorris): Let Disconnect() tell the client that the | 299 // TODO(simonmorris): Let Disconnect() tell the client that the |
| 266 // disconnection was caused by the session exceeding its maximum duration. | 300 // disconnection was caused by the session exceeding its maximum duration. |
| (...skipping 25 matching lines...) Expand all Loading... |
| 292 // Create the object that controls the screen resolution. | 326 // Create the object that controls the screen resolution. |
| 293 screen_controls_ = desktop_environment_->CreateScreenControls(); | 327 screen_controls_ = desktop_environment_->CreateScreenControls(); |
| 294 | 328 |
| 295 // Create the event executor. | 329 // Create the event executor. |
| 296 input_injector_ = desktop_environment_->CreateInputInjector(); | 330 input_injector_ = desktop_environment_->CreateInputInjector(); |
| 297 | 331 |
| 298 // Connect the host clipboard and input stubs. | 332 // Connect the host clipboard and input stubs. |
| 299 host_input_filter_.set_input_stub(input_injector_.get()); | 333 host_input_filter_.set_input_stub(input_injector_.get()); |
| 300 clipboard_echo_filter_.set_host_stub(input_injector_.get()); | 334 clipboard_echo_filter_.set_host_stub(input_injector_.get()); |
| 301 | 335 |
| 302 // Create an AudioScheduler if audio is enabled, to pump audio samples. | |
| 303 if (connection_->session()->config().is_audio_enabled()) { | |
| 304 scoped_ptr<AudioEncoder> audio_encoder = | |
| 305 CreateAudioEncoder(connection_->session()->config()); | |
| 306 audio_scheduler_ = new AudioScheduler( | |
| 307 audio_task_runner_, | |
| 308 network_task_runner_, | |
| 309 desktop_environment_->CreateAudioCapturer(), | |
| 310 audio_encoder.Pass(), | |
| 311 connection_->audio_stub()); | |
| 312 } | |
| 313 | |
| 314 // Create a GnubbyAuthHandler to proxy gnubbyd messages. | 336 // Create a GnubbyAuthHandler to proxy gnubbyd messages. |
| 315 gnubby_auth_handler_ = desktop_environment_->CreateGnubbyAuthHandler( | 337 gnubby_auth_handler_ = desktop_environment_->CreateGnubbyAuthHandler( |
| 316 connection_->client_stub()); | 338 connection_->client_stub()); |
| 317 } | 339 } |
| 318 | 340 |
| 319 void ClientSession::OnConnectionChannelsConnected( | 341 void ClientSession::OnConnectionChannelsConnected( |
| 320 protocol::ConnectionToClient* connection) { | 342 protocol::ConnectionToClient* connection) { |
| 321 DCHECK(CalledOnValidThread()); | 343 DCHECK(CalledOnValidThread()); |
| 322 DCHECK_EQ(connection_.get(), connection); | 344 DCHECK_EQ(connection_.get(), connection); |
| 323 | 345 |
| 324 // Negotiate capabilities with the client. | 346 // Negotiate capabilities with the client. |
| 325 VLOG(1) << "Host capabilities: " << host_capabilities_; | 347 VLOG(1) << "Host capabilities: " << host_capabilities_; |
| 326 | 348 |
| 327 protocol::Capabilities capabilities; | 349 protocol::Capabilities capabilities; |
| 328 capabilities.set_capabilities(host_capabilities_); | 350 capabilities.set_capabilities(host_capabilities_); |
| 329 connection_->client_stub()->SetCapabilities(capabilities); | 351 connection_->client_stub()->SetCapabilities(capabilities); |
| 330 | 352 |
| 331 // Start the event executor. | 353 // Start the event executor. |
| 332 input_injector_->Start(CreateClipboardProxy()); | 354 input_injector_->Start(CreateClipboardProxy()); |
| 333 SetDisableInputs(false); | 355 SetDisableInputs(false); |
| 334 | 356 |
| 335 // Start recording video. | 357 // Start recording video. |
| 336 ResetVideoPipeline(); | 358 ResetVideoPipeline(); |
| 337 | 359 |
| 338 // Start recording audio. | 360 // Create an AudioPump if audio is enabled, to pump audio samples. |
| 339 if (connection_->session()->config().is_audio_enabled()) | 361 if (connection_->session()->config().is_audio_enabled()) { |
| 340 audio_scheduler_->Start(); | 362 scoped_ptr<AudioEncoder> audio_encoder = |
| 363 CreateAudioEncoder(connection_->session()->config()); |
| 364 audio_pump_.reset(new AudioPump(audio_task_runner_, network_task_runner_, |
| 365 desktop_environment_->CreateAudioCapturer(), |
| 366 audio_encoder.Pass(), |
| 367 connection_->audio_stub())); |
| 368 } |
| 341 | 369 |
| 342 // Notify the event handler that all our channels are now connected. | 370 // Notify the event handler that all our channels are now connected. |
| 343 event_handler_->OnSessionChannelsConnected(this); | 371 event_handler_->OnSessionChannelsConnected(this); |
| 344 } | 372 } |
| 345 | 373 |
| 346 void ClientSession::OnConnectionClosed( | 374 void ClientSession::OnConnectionClosed( |
| 347 protocol::ConnectionToClient* connection, | 375 protocol::ConnectionToClient* connection, |
| 348 protocol::ErrorCode error) { | 376 protocol::ErrorCode error) { |
| 349 DCHECK(CalledOnValidThread()); | 377 DCHECK(CalledOnValidThread()); |
| 350 DCHECK_EQ(connection_.get(), connection); | 378 DCHECK_EQ(connection_.get(), connection); |
| 351 | 379 |
| 352 // Ignore any further callbacks. | 380 // Ignore any further callbacks. |
| 353 weak_factory_.InvalidateWeakPtrs(); | 381 weak_factory_.InvalidateWeakPtrs(); |
| 354 | 382 |
| 355 // If the client never authenticated then the session failed. | 383 // If the client never authenticated then the session failed. |
| 356 if (!auth_input_filter_.enabled()) | 384 if (!auth_input_filter_.enabled()) |
| 357 event_handler_->OnSessionAuthenticationFailed(this); | 385 event_handler_->OnSessionAuthenticationFailed(this); |
| 358 | 386 |
| 359 // Block any further input events from the client. | 387 // Block any further input events from the client. |
| 360 // TODO(wez): Fix ChromotingHost::OnSessionClosed not to check our | 388 // TODO(wez): Fix ChromotingHost::OnSessionClosed not to check our |
| 361 // is_authenticated(), so that we can disable |auth_*_filter_| here. | 389 // is_authenticated(), so that we can disable |auth_*_filter_| here. |
| 362 disable_input_filter_.set_enabled(false); | 390 disable_input_filter_.set_enabled(false); |
| 363 disable_clipboard_filter_.set_enabled(false); | 391 disable_clipboard_filter_.set_enabled(false); |
| 364 | 392 |
| 365 // Ensure that any pressed keys or buttons are released. | 393 // Ensure that any pressed keys or buttons are released. |
| 366 input_tracker_.ReleaseAll(); | 394 input_tracker_.ReleaseAll(); |
| 367 | 395 |
| 368 // Stop components access the client, audio or video stubs, which are no | 396 // Stop components access the client, audio or video stubs, which are no |
| 369 // longer valid once ConnectionToClient calls OnConnectionClosed(). | 397 // longer valid once ConnectionToClient calls OnConnectionClosed(). |
| 370 if (audio_scheduler_.get()) { | 398 audio_pump_.reset(); |
| 371 audio_scheduler_->Stop(); | |
| 372 audio_scheduler_ = nullptr; | |
| 373 } | |
| 374 | |
| 375 video_frame_pump_.reset(); | 399 video_frame_pump_.reset(); |
| 376 mouse_shape_pump_.reset(); | 400 mouse_shape_pump_.reset(); |
| 377 | |
| 378 client_clipboard_factory_.InvalidateWeakPtrs(); | 401 client_clipboard_factory_.InvalidateWeakPtrs(); |
| 379 input_injector_.reset(); | 402 input_injector_.reset(); |
| 380 screen_controls_.reset(); | 403 screen_controls_.reset(); |
| 381 desktop_environment_.reset(); | 404 desktop_environment_.reset(); |
| 382 | 405 |
| 383 // Notify the ChromotingHost that this client is disconnected. | 406 // Notify the ChromotingHost that this client is disconnected. |
| 384 // TODO(sergeyu): Log failure reason? | 407 // TODO(sergeyu): Log failure reason? |
| 385 event_handler_->OnSessionClosed(this); | 408 event_handler_->OnSessionClosed(this); |
| 386 } | 409 } |
| 387 | 410 |
| (...skipping 92 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 480 } | 503 } |
| 481 | 504 |
| 482 scoped_ptr<protocol::ClipboardStub> ClientSession::CreateClipboardProxy() { | 505 scoped_ptr<protocol::ClipboardStub> ClientSession::CreateClipboardProxy() { |
| 483 DCHECK(CalledOnValidThread()); | 506 DCHECK(CalledOnValidThread()); |
| 484 | 507 |
| 485 return make_scoped_ptr( | 508 return make_scoped_ptr( |
| 486 new protocol::ClipboardThreadProxy(client_clipboard_factory_.GetWeakPtr(), | 509 new protocol::ClipboardThreadProxy(client_clipboard_factory_.GetWeakPtr(), |
| 487 base::MessageLoopProxy::current())); | 510 base::MessageLoopProxy::current())); |
| 488 } | 511 } |
| 489 | 512 |
| 490 // TODO(sergeyu): Move this to SessionManager? | |
| 491 // static | |
| 492 scoped_ptr<VideoEncoder> ClientSession::CreateVideoEncoder( | |
| 493 const protocol::SessionConfig& config) { | |
| 494 const protocol::ChannelConfig& video_config = config.video_config(); | |
| 495 | |
| 496 if (video_config.codec == protocol::ChannelConfig::CODEC_VP8) { | |
| 497 return remoting::VideoEncoderVpx::CreateForVP8().Pass(); | |
| 498 } else if (video_config.codec == protocol::ChannelConfig::CODEC_VP9) { | |
| 499 return remoting::VideoEncoderVpx::CreateForVP9().Pass(); | |
| 500 } else if (video_config.codec == protocol::ChannelConfig::CODEC_VERBATIM) { | |
| 501 return make_scoped_ptr(new remoting::VideoEncoderVerbatim()); | |
| 502 } | |
| 503 | |
| 504 NOTREACHED(); | |
| 505 return nullptr; | |
| 506 } | |
| 507 | |
| 508 // static | |
| 509 scoped_ptr<AudioEncoder> ClientSession::CreateAudioEncoder( | |
| 510 const protocol::SessionConfig& config) { | |
| 511 const protocol::ChannelConfig& audio_config = config.audio_config(); | |
| 512 | |
| 513 if (audio_config.codec == protocol::ChannelConfig::CODEC_VERBATIM) { | |
| 514 return make_scoped_ptr(new AudioEncoderVerbatim()); | |
| 515 } else if (audio_config.codec == protocol::ChannelConfig::CODEC_OPUS) { | |
| 516 return make_scoped_ptr(new AudioEncoderOpus()); | |
| 517 } | |
| 518 | |
| 519 NOTREACHED(); | |
| 520 return nullptr; | |
| 521 } | |
| 522 | |
| 523 } // namespace remoting | 513 } // namespace remoting |
| OLD | NEW |