diff options
Diffstat (limited to 'chromium/components/mirroring/service')
35 files changed, 3455 insertions, 392 deletions
diff --git a/chromium/components/mirroring/service/BUILD.gn b/chromium/components/mirroring/service/BUILD.gn index a8f92d0058e..a5b3ef6d2f4 100644 --- a/chromium/components/mirroring/service/BUILD.gn +++ b/chromium/components/mirroring/service/BUILD.gn @@ -6,6 +6,7 @@ import("//testing/test.gni") source_set("interface") { sources = [ + "interface.cc", "interface.h", ] @@ -24,14 +25,26 @@ source_set("interface") { source_set("service") { sources = [ + "message_dispatcher.cc", + "message_dispatcher.h", + "mirror_settings.cc", + "mirror_settings.h", + "receiver_response.cc", + "receiver_response.h", "rtp_stream.cc", "rtp_stream.h", "session.cc", "session.h", + "session_monitor.cc", + "session_monitor.h", "udp_socket_client.cc", "udp_socket_client.h", + "value_util.cc", + "value_util.h", "video_capture_client.cc", "video_capture_client.h", + "wifi_status_monitor.cc", + "wifi_status_monitor.h", ] public_deps = [ @@ -40,7 +53,10 @@ source_set("service") { deps = [ ":interface", + "//components/version_info", + "//crypto", "//media", + "//media/capture:capture_base", "//media/capture/mojom:video_capture", "//media/cast:common", "//media/cast:net", @@ -49,6 +65,7 @@ source_set("service") { "//mojo/public/cpp/bindings", "//mojo/public/cpp/system", "//net", + "//services/network/public/cpp", "//services/network/public/mojom", "//ui/gfx", ] @@ -61,10 +78,14 @@ source_set("unittests") { "fake_network_service.h", "fake_video_capture_host.cc", "fake_video_capture_host.h", + "message_dispatcher_unittest.cc", + "receiver_response_unittest.cc", "rtp_stream_unittest.cc", + "session_monitor_unittest.cc", "session_unittest.cc", "udp_socket_client_unittest.cc", "video_capture_client_unittest.cc", + "wifi_status_monitor_unittest.cc", ] deps = [ @@ -81,6 +102,8 @@ source_set("unittests") { "//media/cast:test_support", "//mojo/public/cpp/bindings", "//net", + "//services/network:test_support", + "//services/network/public/cpp", "//services/network/public/mojom", "//testing/gmock", "//testing/gtest", diff --git a/chromium/components/mirroring/service/DEPS b/chromium/components/mirroring/service/DEPS index d924f835b89..ddfc4b1cd47 100644 --- a/chromium/components/mirroring/service/DEPS +++ b/chromium/components/mirroring/service/DEPS @@ -1,4 +1,9 @@ include_rules = [ + "-components", + "+components/mirroring/service", + "+components/version_info", + "+crypto", "+net", - "+services/network/public/mojom", + "+services/network/public", + "+services/network/test", ] diff --git a/chromium/components/mirroring/service/fake_network_service.cc b/chromium/components/mirroring/service/fake_network_service.cc index ca32b975005..971a350aef8 100644 --- a/chromium/components/mirroring/service/fake_network_service.cc +++ b/chromium/components/mirroring/service/fake_network_service.cc @@ -5,6 +5,8 @@ #include "components/mirroring/service/fake_network_service.h" #include "media/cast/test/utility/net_utility.h" +#include "mojo/public/cpp/bindings/strong_binding.h" +#include "services/network/test/test_url_loader_factory.h" namespace mirroring { @@ -63,4 +65,12 @@ void MockNetworkContext::CreateUDPSocket( OnUDPSocketCreated(); } +void MockNetworkContext::CreateURLLoaderFactory( + network::mojom::URLLoaderFactoryRequest request, + network::mojom::URLLoaderFactoryParamsPtr params) { + ASSERT_TRUE(params); + mojo::MakeStrongBinding(std::make_unique<network::TestURLLoaderFactory>(), + std::move(request)); +} + } // namespace mirroring diff --git a/chromium/components/mirroring/service/fake_network_service.h b/chromium/components/mirroring/service/fake_network_service.h index 8237befae49..3612147a52b 100644 --- a/chromium/components/mirroring/service/fake_network_service.h +++ b/chromium/components/mirroring/service/fake_network_service.h @@ -8,8 +8,8 @@ #include "base/callback.h" #include "media/cast/net/cast_transport_config.h" #include "mojo/public/cpp/bindings/binding.h" -#include "services/network/public/mojom/network_service.mojom.h" #include "services/network/public/mojom/udp_socket.mojom.h" +#include "services/network/test/test_network_context.h" #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" @@ -61,7 +61,7 @@ class MockUdpSocket final : public network::mojom::UDPSocket { DISALLOW_COPY_AND_ASSIGN(MockUdpSocket); }; -class MockNetworkContext final : public network::mojom::NetworkContext { +class MockNetworkContext final : public network::TestNetworkContext { public: explicit MockNetworkContext(network::mojom::NetworkContextRequest request); ~MockNetworkContext() override; @@ -69,53 +69,11 @@ class MockNetworkContext final : public network::mojom::NetworkContext { MOCK_METHOD0(OnUDPSocketCreated, void()); // network::mojom::NetworkContext implementation: - void CreateURLLoaderFactory(network::mojom::URLLoaderFactoryRequest request, - uint32_t process_id) override {} - void GetCookieManager(network::mojom::CookieManagerRequest request) override { - } - void GetRestrictedCookieManager( - network::mojom::RestrictedCookieManagerRequest request, - int32_t render_process_id, - int32_t render_frame_id) override {} - void ClearNetworkingHistorySince( - base::Time time, - base::OnceClosure completion_callback) override {} - void ClearHttpCache(base::Time start_time, - base::Time end_time, - network::mojom::ClearCacheUrlFilterPtr filter, - ClearHttpCacheCallback callback) override {} - void SetNetworkConditions( - const std::string& profile_id, - network::mojom::NetworkConditionsPtr conditions) override {} - void SetAcceptLanguage(const std::string& new_accept_language) override {} - void SetCTPolicy( - const std::vector<std::string>& required_hosts, - const std::vector<std::string>& excluded_hosts, - const std::vector<std::string>& excluded_spkis, - const std::vector<std::string>& excluded_legacy_spkis) override {} - void AddHSTSForTesting(const std::string& host, - base::Time expiry, - bool include_subdomains, - AddHSTSForTestingCallback callback) override {} void CreateUDPSocket(network::mojom::UDPSocketRequest request, network::mojom::UDPSocketReceiverPtr receiver) override; - void CreateTCPServerSocket( - const net::IPEndPoint& local_addr, - uint32_t backlog, - const net::MutableNetworkTrafficAnnotationTag& traffic_annotation, - network::mojom::TCPServerSocketRequest request, - CreateTCPServerSocketCallback callback) override {} - void CreateTCPConnectedSocket( - const base::Optional<net::IPEndPoint>& local_addr, - const net::AddressList& remote_addr_list, - const net::MutableNetworkTrafficAnnotationTag& traffic_annotation, - network::mojom::TCPConnectedSocketRequest request, - network::mojom::TCPConnectedSocketObserverPtr observer, - CreateTCPConnectedSocketCallback callback) override {} - void CreateWebSocket(network::mojom::WebSocketRequest request, - int process_id, - int render_frame_id, - const url::Origin& origin) override {} + void CreateURLLoaderFactory( + network::mojom::URLLoaderFactoryRequest request, + network::mojom::URLLoaderFactoryParamsPtr params) override; MockUdpSocket* udp_socket() const { return udp_socket_.get(); } diff --git a/chromium/components/mirroring/service/fake_video_capture_host.cc b/chromium/components/mirroring/service/fake_video_capture_host.cc index 7e61d2da7d6..56a1574a3ba 100644 --- a/chromium/components/mirroring/service/fake_video_capture_host.cc +++ b/chromium/components/mirroring/service/fake_video_capture_host.cc @@ -41,7 +41,10 @@ void FakeVideoCaptureHost::SendOneFrame(const gfx::Size& size, mojo::ScopedSharedBufferHandle buffer = mojo::SharedBufferHandle::Create(5000); memset(buffer->Map(5000).get(), 125, 5000); - observer_->OnBufferCreated(0, std::move(buffer)); + media::mojom::VideoBufferHandlePtr buffer_handle = + media::mojom::VideoBufferHandle::New(); + buffer_handle->set_shared_buffer_handle(std::move(buffer)); + observer_->OnNewBuffer(0, std::move(buffer_handle)); media::VideoFrameMetadata metadata; metadata.SetDouble(media::VideoFrameMetadata::FRAME_RATE, 30); metadata.SetTimeTicks(media::VideoFrameMetadata::REFERENCE_TIME, @@ -49,8 +52,7 @@ void FakeVideoCaptureHost::SendOneFrame(const gfx::Size& size, observer_->OnBufferReady( 0, media::mojom::VideoFrameInfo::New( base::TimeDelta(), metadata.GetInternalValues().Clone(), - media::PIXEL_FORMAT_I420, media::VideoPixelStorage::CPU, size, - gfx::Rect(size))); + media::PIXEL_FORMAT_I420, size, gfx::Rect(size))); } } // namespace mirroring diff --git a/chromium/components/mirroring/service/interface.cc b/chromium/components/mirroring/service/interface.cc new file mode 100644 index 00000000000..6781ef77eac --- /dev/null +++ b/chromium/components/mirroring/service/interface.cc @@ -0,0 +1,15 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/mirroring/service/interface.h" + +namespace mirroring { + +CastSinkInfo::CastSinkInfo() {} + +CastSinkInfo::~CastSinkInfo() {} + +CastSinkInfo::CastSinkInfo(const CastSinkInfo& sink_info) = default; + +} // namespace mirroring diff --git a/chromium/components/mirroring/service/interface.h b/chromium/components/mirroring/service/interface.h index 501280efa0f..3feac5c3edb 100644 --- a/chromium/components/mirroring/service/interface.h +++ b/chromium/components/mirroring/service/interface.h @@ -5,13 +5,10 @@ #ifndef COMPONENTS_MIRRORING_SERVICE_INTERFACE_H_ #define COMPONENTS_MIRRORING_SERVICE_INTERFACE_H_ -#include <vector> +#include <string> -#include "base/callback.h" -#include "base/values.h" #include "media/capture/mojom/video_capture.mojom.h" -#include "media/cast/cast_config.h" -#include "net/base/ip_endpoint.h" +#include "net/base/ip_address.h" #include "services/network/public/mojom/network_service.mojom.h" namespace mirroring { @@ -21,22 +18,55 @@ namespace mirroring { // Errors occurred in a mirroring session. enum SessionError { - SESSION_START_ERROR, // Error occurred while starting. - AUDIO_CAPTURE_ERROR, // Error occurred in audio capturing. - VIDEO_CAPTURE_ERROR, // Error occurred in video capturing. - CAST_STREAMING_ERROR, // Error occurred in cast streaming. - CAST_TRANSPORT_ERROR, // Error occurred in cast transport. + ANSWER_TIME_OUT, // ANSWER timeout. + ANSWER_NOT_OK, // Not OK answer response. + ANSWER_MISMATCHED_CAST_MODE, // ANSWER cast mode mismatched. + ANSWER_MISMATCHED_SSRC_LENGTH, // ANSWER ssrc length mismatched with indexes. + ANSWER_SELECT_MULTIPLE_AUDIO, // Multiple audio streams selected by ANSWER. + ANSWER_SELECT_MULTIPLE_VIDEO, // Multiple video streams selected by ANSWER. + ANSWER_SELECT_INVALID_INDEX, // Invalid index was selected. + ANSWER_NO_AUDIO_OR_VIDEO, // ANSWER not select audio or video. + AUDIO_CAPTURE_ERROR, // Error occurred in audio capturing. + VIDEO_CAPTURE_ERROR, // Error occurred in video capturing. + RTP_STREAM_ERROR, // Error reported by RtpStream. + ENCODING_ERROR, // Error occurred in encoding. + CAST_TRANSPORT_ERROR, // Error occurred in cast transport. }; -enum SessionType { +enum DeviceCapability { AUDIO_ONLY, VIDEO_ONLY, AUDIO_AND_VIDEO, }; -class SessionClient { +constexpr char kRemotingNamespace[] = "urn:x-cast:com.google.cast.remoting"; +constexpr char kWebRtcNamespace[] = "urn:x-cast:com.google.cast.webrtc"; + +struct CastMessage { + std::string message_namespace; + std::string json_format_data; // The content of the message. +}; + +class CastMessageChannel { public: - virtual ~SessionClient() {} + virtual ~CastMessageChannel() {} + virtual void Send(const CastMessage& message) = 0; +}; + +struct CastSinkInfo { + CastSinkInfo(); + ~CastSinkInfo(); + CastSinkInfo(const CastSinkInfo& sink_info); + + net::IPAddress ip_address; + std::string model_name; + std::string friendly_name; + DeviceCapability capability; +}; + +class SessionObserver { + public: + virtual ~SessionObserver() {} // Called when error occurred. The session will be stopped. virtual void OnError(SessionError error) = 0; @@ -46,25 +76,19 @@ class SessionClient { // Called when the session is stopped. virtual void DidStop() = 0; +}; + +class ResourceProvider { + public: + virtual ~ResourceProvider() {} virtual void GetVideoCaptureHost( media::mojom::VideoCaptureHostRequest request) = 0; - virtual void GetNewWorkContext( + virtual void GetNetworkContext( network::mojom::NetworkContextRequest request) = 0; // TODO(xjz): Add interface to get AudioCaptureHost. // TODO(xjz): Add interface for HW encoder profiles query and VEA create // support. - - // TODO(xjz): Change this with an interface to send/receive messages to/from - // receiver through cast channel, and generate/parse the OFFER/ANSWER message - // in Mirroing service. - using GetAnswerCallback = base::OnceCallback<void( - const media::cast::FrameSenderConfig& audio_config, - const media::cast::FrameSenderConfig& video_config)>; - virtual void DoOfferAnswerExchange( - const std::vector<media::cast::FrameSenderConfig>& audio_configs, - const std::vector<media::cast::FrameSenderConfig>& video_configs, - GetAnswerCallback callback) = 0; }; } // namespace mirroring diff --git a/chromium/components/mirroring/service/message_dispatcher.cc b/chromium/components/mirroring/service/message_dispatcher.cc new file mode 100644 index 00000000000..aa5ad4a4069 --- /dev/null +++ b/chromium/components/mirroring/service/message_dispatcher.cc @@ -0,0 +1,157 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/mirroring/service/message_dispatcher.h" + +#include "base/bind_helpers.h" +#include "base/logging.h" +#include "base/rand_util.h" + +namespace mirroring { + +// Holds a request until |timeout| elapses or an acceptable response is +// received. When timeout, |response_callback| runs with an UNKNOWN type +// response. +class MessageDispatcher::RequestHolder { + public: + RequestHolder() {} + + ~RequestHolder() { + if (!response_callback_.is_null()) + std::move(response_callback_).Run(ReceiverResponse()); + } + + void Start(const base::TimeDelta& timeout, + int32_t sequence_number, + OnceResponseCallback response_callback) { + response_callback_ = std::move(response_callback); + sequence_number_ = sequence_number; + DCHECK(!response_callback_.is_null()); + timer_.Start( + FROM_HERE, timeout, + base::BindRepeating(&RequestHolder::SendResponse, + base::Unretained(this), ReceiverResponse())); + } + + // Send |response| if the sequence number matches, or if the request times + // out, in which case the |response| is UNKNOWN type. + void SendResponse(const ReceiverResponse& response) { + if (!timer_.IsRunning() || response.sequence_number == sequence_number_) + std::move(response_callback_).Run(response); + // Ignore the response with mismatched sequence number. + } + + private: + OnceResponseCallback response_callback_; + base::OneShotTimer timer_; + int32_t sequence_number_ = -1; + + DISALLOW_COPY_AND_ASSIGN(RequestHolder); +}; + +MessageDispatcher::MessageDispatcher(CastMessageChannel* outbound_channel, + ErrorCallback error_callback) + : outbound_channel_(outbound_channel), + error_callback_(std::move(error_callback)), + last_sequence_number_(base::RandInt(0, 1e9)) { + DCHECK(outbound_channel_); + DCHECK(!error_callback_.is_null()); +} + +MessageDispatcher::~MessageDispatcher() { + // Prevent the re-entrant operations on |callback_map_|. + decltype(callback_map_) subscriptions; + subscriptions.swap(callback_map_); + subscriptions.clear(); +} + +void MessageDispatcher::Send(const CastMessage& message) { + if (message.message_namespace != kWebRtcNamespace && + message.message_namespace != kRemotingNamespace) { + DVLOG(2) << "Ignore message with unknown namespace = " + << message.message_namespace; + return; // Ignore message with wrong namespace. + } + if (message.json_format_data.empty()) + return; // Ignore null message. + + ReceiverResponse response; + if (!response.Parse(message.json_format_data)) { + error_callback_.Run("Response parsing error. message=" + + message.json_format_data); + return; + } + +#if DCHECK_IS_ON() + if (response.type == ResponseType::RPC) + DCHECK_EQ(kRemotingNamespace, message.message_namespace); + else + DCHECK_EQ(kWebRtcNamespace, message.message_namespace); +#endif // DCHECK_IS_ON() + + const auto callback_iter = callback_map_.find(response.type); + if (callback_iter == callback_map_.end()) { + error_callback_.Run("No callback subscribed. message=" + + message.json_format_data); + return; + } + callback_iter->second.Run(response); +} + +void MessageDispatcher::Subscribe(ResponseType type, + ResponseCallback callback) { + DCHECK(type != ResponseType::UNKNOWN); + DCHECK(!callback.is_null()); + + const auto insert_result = + callback_map_.emplace(std::make_pair(type, std::move(callback))); + DCHECK(insert_result.second); +} + +void MessageDispatcher::Unsubscribe(ResponseType type) { + auto iter = callback_map_.find(type); + if (iter != callback_map_.end()) + callback_map_.erase(iter); +} + +int32_t MessageDispatcher::GetNextSeqNumber() { + // Skip 0, which is used by Cast receiver to indicate that the broadcast + // status message is not coming from a specific sender (it is an autonomous + // status change, not triggered by a command from any sender). Strange usage + // of 0 though; could be a null / optional field. + return ++last_sequence_number_; +} + +void MessageDispatcher::SendOutboundMessage(const CastMessage& message) { + outbound_channel_->Send(message); +} + +void MessageDispatcher::RequestReply(const CastMessage& message, + ResponseType response_type, + int32_t sequence_number, + const base::TimeDelta& timeout, + OnceResponseCallback callback) { + DCHECK(!callback.is_null()); + DCHECK(timeout > base::TimeDelta()); + RequestHolder* const request_holder = new RequestHolder(); + request_holder->Start( + timeout, sequence_number, + base::BindOnce( + [](MessageDispatcher* dispatcher, ResponseType response_type, + OnceResponseCallback callback, const ReceiverResponse& response) { + dispatcher->Unsubscribe(response_type); + std::move(callback).Run(response); + }, + this, response_type, std::move(callback))); + // |request_holder| keeps alive until the callback is unsubscribed. + Subscribe(response_type, base::BindRepeating( + [](RequestHolder* request_holder, + const ReceiverResponse& response) { + request_holder->SendResponse(response); + }, + base::Owned(request_holder))); + SendOutboundMessage(message); +} + +} // namespace mirroring diff --git a/chromium/components/mirroring/service/message_dispatcher.h b/chromium/components/mirroring/service/message_dispatcher.h new file mode 100644 index 00000000000..7b2ee9bfb62 --- /dev/null +++ b/chromium/components/mirroring/service/message_dispatcher.h @@ -0,0 +1,73 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_MIRRORING_SERVICE_MESSAGE_DISPATCHER_H_ +#define COMPONENTS_MIRRORING_SERVICE_MESSAGE_DISPATCHER_H_ + +#include "base/callback.h" +#include "base/containers/flat_map.h" +#include "base/macros.h" +#include "components/mirroring/service/interface.h" +#include "components/mirroring/service/receiver_response.h" + +namespace mirroring { + +// Dispatches inbound/outbound messages. The outbound messages are sent out +// through |outbound_channel|, and the inbound messages are handled by this +// class. +class MessageDispatcher final : public CastMessageChannel { + public: + using ErrorCallback = base::RepeatingCallback<void(const std::string&)>; + // TODO(xjz): Also pass a CastMessageChannel interface request for inbound + // message channel. + MessageDispatcher(CastMessageChannel* outbound_channel, + ErrorCallback error_callback); + ~MessageDispatcher() override; + + using ResponseCallback = + base::RepeatingCallback<void(const ReceiverResponse& response)>; + // Registers/Unregisters callback for a certain type of responses. + void Subscribe(ResponseType type, ResponseCallback callback); + void Unsubscribe(ResponseType type); + + using OnceResponseCallback = + base::OnceCallback<void(const ReceiverResponse& response)>; + // Sends the given message and subscribes to replies until an acceptable one + // is received or a timeout elapses. Message of the given response type is + // delivered to the supplied callback if the sequence number of the response + // matches |sequence_number|. If the timeout period elapses, the callback will + // be run once with an unknown type of |response|. + void RequestReply(const CastMessage& message, + ResponseType response_type, + int32_t sequence_number, + const base::TimeDelta& timeout, + OnceResponseCallback callback); + + // Get the sequence number for the next outbound message. Never returns 0. + int32_t GetNextSeqNumber(); + + // Requests to send outbound |message|. + void SendOutboundMessage(const CastMessage& message); + + private: + class RequestHolder; + + // CastMessageChannel implementation. Handles inbound messages. + void Send(const CastMessage& message) override; + + // Takes care of sending outbound messages. + CastMessageChannel* const outbound_channel_; + const ErrorCallback error_callback_; + + int32_t last_sequence_number_; + + // Holds callbacks for different types of responses. + base::flat_map<ResponseType, ResponseCallback> callback_map_; + + DISALLOW_COPY_AND_ASSIGN(MessageDispatcher); +}; + +} // namespace mirroring + +#endif // COMPONENTS_MIRRORING_SERVICE_MESSAGE_DISPATCHER_H_ diff --git a/chromium/components/mirroring/service/message_dispatcher_unittest.cc b/chromium/components/mirroring/service/message_dispatcher_unittest.cc new file mode 100644 index 00000000000..32cb7328ecc --- /dev/null +++ b/chromium/components/mirroring/service/message_dispatcher_unittest.cc @@ -0,0 +1,299 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/mirroring/service/message_dispatcher.h" + +#include "base/base64.h" +#include "base/bind.h" +#include "base/callback.h" +#include "base/macros.h" +#include "base/run_loop.h" +#include "base/test/mock_callback.h" +#include "base/test/scoped_task_environment.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using ::testing::InvokeWithoutArgs; +using ::testing::_; + +namespace mirroring { + +namespace { + +bool IsEqual(const CastMessage& message1, const CastMessage& message2) { + return message1.message_namespace == message2.message_namespace && + message1.json_format_data == message2.json_format_data; +} + +void CloneResponse(const ReceiverResponse& response, + ReceiverResponse* cloned_response) { + cloned_response->type = response.type; + cloned_response->session_id = response.session_id; + cloned_response->sequence_number = response.sequence_number; + cloned_response->result = response.result; + if (response.answer) + cloned_response->answer = std::make_unique<Answer>(*response.answer); + if (response.status) + cloned_response->status = + std::make_unique<ReceiverStatus>(*response.status); + if (response.capabilities) { + cloned_response->capabilities = + std::make_unique<ReceiverCapability>(*response.capabilities); + } + cloned_response->rpc = response.rpc; + if (response.error) { + cloned_response->error = std::make_unique<ReceiverError>(); + cloned_response->error->code = response.error->code; + cloned_response->error->description = response.error->description; + cloned_response->error->details = response.error->details; + } +} + +} // namespace + +class MessageDispatcherTest : public CastMessageChannel, + public ::testing::Test { + public: + MessageDispatcherTest() { + message_dispatcher_ = std::make_unique<MessageDispatcher>( + this, base::BindRepeating(&MessageDispatcherTest::OnParsingError, + base::Unretained(this))); + message_dispatcher_->Subscribe( + ResponseType::ANSWER, + base::BindRepeating(&MessageDispatcherTest::OnAnswerResponse, + base::Unretained(this))); + message_dispatcher_->Subscribe( + ResponseType::STATUS_RESPONSE, + base::BindRepeating(&MessageDispatcherTest::OnStatusResponse, + base::Unretained(this))); + } + ~MessageDispatcherTest() override { scoped_task_environment_.RunUntilIdle(); } + + void OnParsingError(const std::string& error_message) { + last_error_message_ = error_message; + } + + void OnAnswerResponse(const ReceiverResponse& response) { + if (!last_answer_response_) + last_answer_response_ = std::make_unique<ReceiverResponse>(); + CloneResponse(response, last_answer_response_.get()); + } + + void OnStatusResponse(const ReceiverResponse& response) { + if (!last_status_response_) + last_status_response_ = std::make_unique<ReceiverResponse>(); + CloneResponse(response, last_status_response_.get()); + } + + protected: + // CastMessageChannel implementation. Handles outbound message. + void Send(const CastMessage& message) override { + last_outbound_message_.message_namespace = message.message_namespace; + last_outbound_message_.json_format_data = message.json_format_data; + } + + // Simulates receiving an inbound message from receiver. + void SendInboundMessage(const CastMessage& message) { + CastMessageChannel* inbound_message_channel = message_dispatcher_.get(); + inbound_message_channel->Send(message); + } + + base::test::ScopedTaskEnvironment scoped_task_environment_; + std::unique_ptr<MessageDispatcher> message_dispatcher_; + CastMessage last_outbound_message_; + std::string last_error_message_; + std::unique_ptr<ReceiverResponse> last_answer_response_; + std::unique_ptr<ReceiverResponse> last_status_response_; + + private: + DISALLOW_COPY_AND_ASSIGN(MessageDispatcherTest); +}; + +TEST_F(MessageDispatcherTest, SendsOutboundMessage) { + const std::string test1 = "{\"a\": 1, \"b\": 2}"; + const CastMessage message1 = CastMessage{kWebRtcNamespace, test1}; + message_dispatcher_->SendOutboundMessage(message1); + scoped_task_environment_.RunUntilIdle(); + EXPECT_TRUE(IsEqual(message1, last_outbound_message_)); + EXPECT_TRUE(last_error_message_.empty()); + + const std::string test2 = "{\"m\": 99, \"i\": 98, \"u\": 97}"; + const CastMessage message2 = CastMessage{kWebRtcNamespace, test2}; + message_dispatcher_->SendOutboundMessage(message2); + scoped_task_environment_.RunUntilIdle(); + EXPECT_TRUE(IsEqual(message2, last_outbound_message_)); + EXPECT_TRUE(last_error_message_.empty()); +} + +TEST_F(MessageDispatcherTest, DispatchMessageToSubscriber) { + // Simulate a receiver ANSWER response and expect that just the ANSWER + // subscriber processes the message. + const std::string answer_response = + "{\"type\":\"ANSWER\",\"seqNum\":12345,\"result\":\"ok\"," + "\"answer\":{\"udpPort\":50691}}"; + const CastMessage answer_message = + CastMessage{kWebRtcNamespace, answer_response}; + SendInboundMessage(answer_message); + scoped_task_environment_.RunUntilIdle(); + ASSERT_TRUE(last_answer_response_); + EXPECT_FALSE(last_status_response_); + EXPECT_EQ(12345, last_answer_response_->sequence_number); + EXPECT_EQ(ResponseType::ANSWER, last_answer_response_->type); + EXPECT_EQ("ok", last_answer_response_->result); + EXPECT_EQ(50691, last_answer_response_->answer->udp_port); + EXPECT_FALSE(last_answer_response_->status); + last_answer_response_.reset(); + EXPECT_TRUE(last_error_message_.empty()); + + // Simulate a receiver STATUS_RESPONSE and expect that just the + // STATUS_RESPONSE subscriber processes the message. + const std::string status_response = + "{\"type\":\"STATUS_RESPONSE\",\"seqNum\":12345,\"result\":\"ok\"," + "\"status\":{\"wifiSnr\":42}}"; + const CastMessage status_message = + CastMessage{kWebRtcNamespace, status_response}; + SendInboundMessage(status_message); + scoped_task_environment_.RunUntilIdle(); + EXPECT_FALSE(last_answer_response_); + ASSERT_TRUE(last_status_response_); + EXPECT_EQ(12345, last_status_response_->sequence_number); + EXPECT_EQ(ResponseType::STATUS_RESPONSE, last_status_response_->type); + EXPECT_EQ("ok", last_status_response_->result); + EXPECT_EQ(42, last_status_response_->status->wifi_snr); + last_status_response_.reset(); + EXPECT_TRUE(last_error_message_.empty()); + + // Unsubscribe from ANSWER messages, and when feeding-in an ANSWER message, + // nothing should happen. + message_dispatcher_->Unsubscribe(ResponseType::ANSWER); + SendInboundMessage(answer_message); + scoped_task_environment_.RunUntilIdle(); + EXPECT_FALSE(last_answer_response_); + EXPECT_FALSE(last_status_response_); + EXPECT_FALSE(last_error_message_.empty()); // Expect an error reported. + last_error_message_.clear(); + + // However, STATUS_RESPONSE messages should still be dispatcher to the + // remaining subscriber. + SendInboundMessage(status_message); + scoped_task_environment_.RunUntilIdle(); + EXPECT_FALSE(last_answer_response_); + EXPECT_TRUE(last_status_response_); + last_status_response_.reset(); + EXPECT_TRUE(last_error_message_.empty()); + + // Finally, unsubscribe from STATUS_RESPONSE messages, and when feeding-in + // either an ANSWER or a STATUS_RESPONSE message, nothing should happen. + message_dispatcher_->Unsubscribe(ResponseType::STATUS_RESPONSE); + SendInboundMessage(answer_message); + scoped_task_environment_.RunUntilIdle(); + EXPECT_FALSE(last_answer_response_); + EXPECT_FALSE(last_status_response_); + EXPECT_FALSE(last_error_message_.empty()); + last_error_message_.clear(); + SendInboundMessage(status_message); + scoped_task_environment_.RunUntilIdle(); + EXPECT_FALSE(last_answer_response_); + EXPECT_FALSE(last_status_response_); + EXPECT_FALSE(last_error_message_.empty()); +} + +TEST_F(MessageDispatcherTest, IgnoreMalformedMessage) { + const CastMessage message = + CastMessage{kWebRtcNamespace, "MUAHAHAHAHAHAHAHA!"}; + SendInboundMessage(message); + scoped_task_environment_.RunUntilIdle(); + EXPECT_FALSE(last_answer_response_); + EXPECT_FALSE(last_status_response_); + EXPECT_FALSE(last_error_message_.empty()); +} + +TEST_F(MessageDispatcherTest, IgnoreMessageWithWrongNamespace) { + const std::string answer_response = + "{\"type\":\"ANSWER\",\"seqNum\":12345,\"result\":\"ok\"," + "\"answer\":{\"udpPort\":50691}}"; + const CastMessage answer_message = + CastMessage{"Wrong_namespace", answer_response}; + SendInboundMessage(answer_message); + scoped_task_environment_.RunUntilIdle(); + EXPECT_FALSE(last_answer_response_); + EXPECT_FALSE(last_status_response_); + // Messages with different namespace are ignored with no error reported. + EXPECT_TRUE(last_error_message_.empty()); +} + +TEST_F(MessageDispatcherTest, RequestReply) { + EXPECT_FALSE(last_answer_response_); + EXPECT_FALSE(last_status_response_); + message_dispatcher_->Unsubscribe(ResponseType::ANSWER); + scoped_task_environment_.RunUntilIdle(); + const std::string fake_offer = "{\"type\":\"OFFER\",\"seqNum\":45623}"; + const CastMessage offer_message = CastMessage{kWebRtcNamespace, fake_offer}; + message_dispatcher_->RequestReply( + offer_message, ResponseType::ANSWER, 45623, + base::TimeDelta::FromMilliseconds(100), + base::BindRepeating(&MessageDispatcherTest::OnAnswerResponse, + base::Unretained(this))); + scoped_task_environment_.RunUntilIdle(); + // Received the request to send the outbound message. + EXPECT_TRUE(IsEqual(offer_message, last_outbound_message_)); + + std::string answer_response = + "{\"type\":\"ANSWER\",\"seqNum\":12345,\"result\":\"ok\"," + "\"answer\":{\"udpPort\":50691}}"; + CastMessage answer_message = CastMessage{kWebRtcNamespace, answer_response}; + SendInboundMessage(answer_message); + scoped_task_environment_.RunUntilIdle(); + // The answer message with mismatched sequence number is ignored. + EXPECT_FALSE(last_answer_response_); + EXPECT_FALSE(last_status_response_); + EXPECT_TRUE(last_error_message_.empty()); + + answer_response = + "{\"type\":\"ANSWER\",\"seqNum\":45623,\"result\":\"ok\"," + "\"answer\":{\"udpPort\":50691}}"; + answer_message = CastMessage{kWebRtcNamespace, answer_response}; + SendInboundMessage(answer_message); + scoped_task_environment_.RunUntilIdle(); + ASSERT_TRUE(last_answer_response_); + EXPECT_FALSE(last_status_response_); + EXPECT_TRUE(last_error_message_.empty()); + EXPECT_EQ(45623, last_answer_response_->sequence_number); + EXPECT_EQ(ResponseType::ANSWER, last_answer_response_->type); + EXPECT_EQ("ok", last_answer_response_->result); + EXPECT_EQ(50691, last_answer_response_->answer->udp_port); + last_answer_response_.reset(); + + // Expect that the callback for ANSWER message was already unsubscribed. + SendInboundMessage(answer_message); + scoped_task_environment_.RunUntilIdle(); + EXPECT_FALSE(last_answer_response_); + EXPECT_FALSE(last_status_response_); + EXPECT_FALSE(last_error_message_.empty()); + last_error_message_.clear(); + + const CastMessage fake_message = + CastMessage{kWebRtcNamespace, "{\"type\":\"OFFER\",\"seqNum\":12345}"}; + message_dispatcher_->RequestReply( + fake_message, ResponseType::ANSWER, 12345, + base::TimeDelta::FromMilliseconds(100), + base::BindRepeating(&MessageDispatcherTest::OnAnswerResponse, + base::Unretained(this))); + scoped_task_environment_.RunUntilIdle(); + // Received the request to send the outbound message. + EXPECT_TRUE(IsEqual(fake_message, last_outbound_message_)); + EXPECT_FALSE(last_answer_response_); + EXPECT_FALSE(last_status_response_); + + // Destroy the dispatcher. Expect to receive an unknown type response. + message_dispatcher_.reset(); + scoped_task_environment_.RunUntilIdle(); + ASSERT_TRUE(last_answer_response_); + EXPECT_FALSE(last_status_response_); + EXPECT_TRUE(last_error_message_.empty()); + EXPECT_EQ(ResponseType::UNKNOWN, last_answer_response_->type); + EXPECT_EQ(-1, last_answer_response_->sequence_number); +} + +} // namespace mirroring diff --git a/chromium/components/mirroring/service/mirror_settings.cc b/chromium/components/mirroring/service/mirror_settings.cc new file mode 100644 index 00000000000..a14799757a4 --- /dev/null +++ b/chromium/components/mirroring/service/mirror_settings.cc @@ -0,0 +1,148 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/mirroring/service/mirror_settings.h" + +#include <algorithm> + +using media::cast::FrameSenderConfig; +using media::cast::Codec; +using media::cast::RtpPayloadType; +using media::ResolutionChangePolicy; + +namespace mirroring { + +namespace { + +// Starting end-to-end latency for animated content. +constexpr base::TimeDelta kAnimatedPlayoutDelay = + base::TimeDelta::FromMilliseconds(400); + +// Minimum end-to-end latency. This allows cast streaming to adaptively lower +// latency in interactive streaming scenarios. +// TODO(miu): This was 120 before stable launch, but we got user feedback that +// this was causing audio drop-outs. So, we need to fix the Cast Streaming +// implementation before lowering this setting. +constexpr base::TimeDelta kMinPlayoutDelay = + base::TimeDelta::FromMilliseconds(400); + +// Maximum end-to-end latency. +constexpr base::TimeDelta kMaxPlayoutDelay = + base::TimeDelta::FromMilliseconds(800); + +constexpr int kAudioTimebase = 48000; +constexpr int kVidoTimebase = 90000; +constexpr int kAudioChannels = 2; +constexpr int kAudioFramerate = 100; // 100 FPS for 10ms packets. +constexpr int kMinVideoBitrate = 300000; +constexpr int kMaxVideoBitrate = 5000000; +constexpr int kAudioBitrate = 0; // 0 means automatic. +constexpr int kMaxFrameRate = 30; // The maximum frame rate for captures. +constexpr int kMaxWidth = 1920; // Maximum video width in pixels. +constexpr int kMaxHeight = 1080; // Maximum video height in pixels. +constexpr int kMinWidth = 180; // Minimum video frame width in pixels. +constexpr int kMinHeight = 180; // Minimum video frame height in pixels. + +} // namespace + +MirrorSettings::MirrorSettings() + : min_width_(kMinWidth), + min_height_(kMinHeight), + max_width_(kMaxWidth), + max_height_(kMaxHeight) {} + +MirrorSettings::~MirrorSettings() {} + +// static +FrameSenderConfig MirrorSettings::GetDefaultAudioConfig( + RtpPayloadType payload_type, + Codec codec) { + FrameSenderConfig config; + config.sender_ssrc = 1; + config.receiver_ssrc = 2; + config.min_playout_delay = kMinPlayoutDelay; + config.max_playout_delay = kMaxPlayoutDelay; + config.animated_playout_delay = kAnimatedPlayoutDelay; + config.rtp_payload_type = payload_type; + config.rtp_timebase = kAudioTimebase; + config.channels = kAudioChannels; + config.min_bitrate = config.max_bitrate = config.start_bitrate = + kAudioBitrate; + config.max_frame_rate = kAudioFramerate; // 10 ms audio frames + config.codec = codec; + return config; +} + +// static +FrameSenderConfig MirrorSettings::GetDefaultVideoConfig( + RtpPayloadType payload_type, + Codec codec) { + FrameSenderConfig config; + config.sender_ssrc = 11; + config.receiver_ssrc = 12; + config.min_playout_delay = kMinPlayoutDelay; + config.max_playout_delay = kMaxPlayoutDelay; + config.animated_playout_delay = kAnimatedPlayoutDelay; + config.rtp_payload_type = payload_type; + config.rtp_timebase = kVidoTimebase; + config.channels = 1; + config.min_bitrate = kMinVideoBitrate; + config.max_bitrate = kMaxVideoBitrate; + config.start_bitrate = kMinVideoBitrate; + config.max_frame_rate = kMaxFrameRate; + config.codec = codec; + return config; +} + +void MirrorSettings::SetResolutionContraints(int max_width, int max_height) { + max_width_ = std::max(max_width, min_width_); + max_height_ = std::max(max_height, min_height_); +} + +media::VideoCaptureParams MirrorSettings::GetVideoCaptureParams() { + media::VideoCaptureParams params; + params.requested_format = + media::VideoCaptureFormat(gfx::Size(max_width_, max_height_), + kMaxFrameRate, media::PIXEL_FORMAT_I420); + if (max_height_ == min_height_ && max_width_ == min_width_) { + params.resolution_change_policy = ResolutionChangePolicy::FIXED_RESOLUTION; + } else if ((100 * min_width_ / min_height_) == + (100 * max_width_ / max_height_)) { + params.resolution_change_policy = + ResolutionChangePolicy::FIXED_ASPECT_RATIO; + } else { + params.resolution_change_policy = ResolutionChangePolicy::ANY_WITHIN_LIMIT; + } + DCHECK(params.IsValid()); + return params; +} + +base::Value MirrorSettings::ToDictionaryValue() { + base::Value settings(base::Value::Type::DICTIONARY); + settings.SetKey("maxWidth", base::Value(max_width_)); + settings.SetKey("maxHeight", base::Value(max_height_)); + settings.SetKey("minWidth", base::Value(min_width_)); + settings.SetKey("minHeight", base::Value(min_height_)); + settings.SetKey("senderSideLetterboxing", base::Value(true)); + settings.SetKey("minFrameRate", base::Value(0)); + settings.SetKey("maxFrameRate", base::Value(kMaxFrameRate)); + settings.SetKey("minVideoBitrate", base::Value(kMinVideoBitrate)); + settings.SetKey("maxVideoBitrate", base::Value(kMaxVideoBitrate)); + settings.SetKey("audioBitrate", base::Value(kAudioBitrate)); + settings.SetKey( + "maxLatencyMillis", + base::Value(static_cast<int32_t>(kMaxPlayoutDelay.InMilliseconds()))); + settings.SetKey( + "minLatencyMillis", + base::Value(static_cast<int32_t>(kMinPlayoutDelay.InMilliseconds()))); + settings.SetKey("animatedLatencyMillis", + base::Value(static_cast<int32_t>( + kAnimatedPlayoutDelay.InMilliseconds()))); + settings.SetKey("dscpEnabled", base::Value(false)); + settings.SetKey("enableLogging", base::Value(true)); + settings.SetKey("useTdls", base::Value(false)); + return settings; +} + +} // namespace mirroring diff --git a/chromium/components/mirroring/service/mirror_settings.h b/chromium/components/mirroring/service/mirror_settings.h new file mode 100644 index 00000000000..937602d3e36 --- /dev/null +++ b/chromium/components/mirroring/service/mirror_settings.h @@ -0,0 +1,57 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_MIRRORING_SERVICE_MIRROR_SETTINGS_H_ +#define COMPONENTS_MIRRORING_SERVICE_MIRROR_SETTINGS_H_ + +#include "base/time/time.h" +#include "base/values.h" +#include "media/capture/video_capture_types.h" +#include "media/cast/cast_config.h" + +namespace mirroring { + +// Holds the default settings for a mirroring session. This class provides the +// audio/video configs that this sender supports. And also provides the +// audio/video constraints used for capturing. +// TODO(xjz): Add the function to generate the audio capture contraints. +// TODO(xjz): Add setters to the settings that might be overriden by integration +// tests. +class MirrorSettings { + public: + MirrorSettings(); + ~MirrorSettings(); + + // Get the audio/video config with given codec. + static media::cast::FrameSenderConfig GetDefaultAudioConfig( + media::cast::RtpPayloadType payload_type, + media::cast::Codec codec); + static media::cast::FrameSenderConfig GetDefaultVideoConfig( + media::cast::RtpPayloadType payload_type, + media::cast::Codec codec); + + // Call to override the default resolution settings. + void SetResolutionContraints(int max_width, int max_height); + + // Get video capture constraints with the current settings. + media::VideoCaptureParams GetVideoCaptureParams(); + + int max_width() const { return max_width_; } + int max_height() const { return max_height_; } + + // Returns a dictionary value of the current settings. + base::Value ToDictionaryValue(); + + private: + const int min_width_; + const int min_height_; + int max_width_; + int max_height_; + + DISALLOW_COPY_AND_ASSIGN(MirrorSettings); +}; + +} // namespace mirroring + +#endif // COMPONENTS_MIRRORING_SERVICE_MIRROR_SETTINGS_H_ diff --git a/chromium/components/mirroring/service/receiver_response.cc b/chromium/components/mirroring/service/receiver_response.cc new file mode 100644 index 00000000000..6feff42172c --- /dev/null +++ b/chromium/components/mirroring/service/receiver_response.cc @@ -0,0 +1,199 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/mirroring/service/receiver_response.h" + +#include "base/base64.h" +#include "base/json/json_reader.h" +#include "base/json/json_writer.h" +#include "base/logging.h" +#include "base/strings/string_util.h" +#include "components/mirroring/service/value_util.h" + +namespace mirroring { + +namespace { + +// Get the response type from the type string value in the JSON message. +ResponseType GetResponseType(const std::string& type) { + if (type == "ANSWER") + return ResponseType::ANSWER; + if (type == "STATUS_RESPONSE") + return ResponseType::STATUS_RESPONSE; + if (type == "CAPABILITIES_RESPONSE") + return ResponseType::CAPABILITIES_RESPONSE; + if (type == "RPC") + return ResponseType::RPC; + return ResponseType::UNKNOWN; +} + +} // namespace + +Answer::Answer() + : udp_port(-1), supports_get_status(false), cast_mode("mirroring") {} + +Answer::~Answer() {} + +Answer::Answer(const Answer& answer) = default; + +bool Answer::Parse(const base::Value& raw_value) { + return (raw_value.is_dict() && GetInt(raw_value, "udpPort", &udp_port) && + GetIntArray(raw_value, "ssrcs", &ssrcs) && + GetIntArray(raw_value, "sendIndexes", &send_indexes) && + GetString(raw_value, "IV", &iv) && + GetBool(raw_value, "receiverGetStatus", &supports_get_status) && + GetString(raw_value, "castMode", &cast_mode)); +} + +// ---------------------------------------------------------------------------- + +ReceiverStatus::ReceiverStatus() : wifi_snr(0) {} + +ReceiverStatus::~ReceiverStatus() {} + +ReceiverStatus::ReceiverStatus(const ReceiverStatus& status) = default; + +bool ReceiverStatus::Parse(const base::Value& raw_value) { + return (raw_value.is_dict() && GetDouble(raw_value, "wifiSnr", &wifi_snr) && + GetIntArray(raw_value, "wifiSpeed", &wifi_speed)); +} + +// ---------------------------------------------------------------------------- + +ReceiverKeySystem::ReceiverKeySystem() {} + +ReceiverKeySystem::~ReceiverKeySystem() {} + +ReceiverKeySystem::ReceiverKeySystem( + const ReceiverKeySystem& receiver_key_system) = default; + +bool ReceiverKeySystem::Parse(const base::Value& raw_value) { + return (raw_value.is_dict() && GetString(raw_value, "keySystemName", &name) && + GetStringArray(raw_value, "initDataTypes", &init_data_types) && + GetStringArray(raw_value, "codecs", &codecs) && + GetStringArray(raw_value, "secureCodecs", &secure_codecs) && + GetStringArray(raw_value, "audioRobustness", &audio_robustness) && + GetStringArray(raw_value, "videoRobustness", &video_robustness) && + GetString(raw_value, "persistentLicenseSessionSupport", + &persistent_license_session_support) && + GetString(raw_value, "persistentReleaseMessageSessionSupport", + &persistent_release_message_session_support) && + GetString(raw_value, "persistentStateSupport", + &persistent_state_support) && + GetString(raw_value, "distinctiveIdentifierSupport", + &distinctive_identifier_support)); +} + +// ---------------------------------------------------------------------------- + +ReceiverCapability::ReceiverCapability() {} + +ReceiverCapability::~ReceiverCapability() {} + +ReceiverCapability::ReceiverCapability(const ReceiverCapability& capabilities) = + default; + +bool ReceiverCapability::Parse(const base::Value& raw_value) { + if (!raw_value.is_dict() || + !GetStringArray(raw_value, "mediaCaps", &media_caps)) + return false; + auto* found = raw_value.FindKey("keySystems"); + if (!found) + return true; + for (const auto& key_system_value : found->GetList()) { + ReceiverKeySystem key_system; + if (!key_system.Parse(key_system_value)) + return false; + key_systems.emplace_back(key_system); + } + return true; +} + +// ---------------------------------------------------------------------------- + +ReceiverError::ReceiverError() : code(-1) {} + +ReceiverError::~ReceiverError() {} + +bool ReceiverError::Parse(const base::Value& raw_value) { + if (!raw_value.is_dict() || !GetInt(raw_value, "code", &code) || + !GetString(raw_value, "description", &description)) + return false; + auto* found = raw_value.FindKey("details"); + return found && base::JSONWriter::Write(*found, &details); +} + +// ---------------------------------------------------------------------------- + +ReceiverResponse::ReceiverResponse() + : type(ResponseType::UNKNOWN), session_id(-1), sequence_number(-1) {} + +ReceiverResponse::~ReceiverResponse() {} + +ReceiverResponse::ReceiverResponse(ReceiverResponse&& receiver_response) = + default; + +ReceiverResponse& ReceiverResponse::operator=( + ReceiverResponse&& receiver_response) = default; + +bool ReceiverResponse::Parse(const std::string& message_data) { + std::unique_ptr<base::Value> raw_value = base::JSONReader::Read(message_data); + if (!raw_value || !raw_value->is_dict() || + !GetInt(*raw_value, "sessionId", &session_id) || + !GetInt(*raw_value, "seqNum", &sequence_number) || + !GetString(*raw_value, "result", &result)) + return false; + + if (result == "error") { + auto* found = raw_value->FindKey("error"); + if (found) { + error = std::make_unique<ReceiverError>(); + if (!error->Parse(*found)) + return false; + } + } + + std::string message_type; + if (!GetString(*raw_value, "type", &message_type)) + return false; + // Convert |message_type| to uppercase. + message_type = base::ToUpperASCII(message_type); + type = GetResponseType(message_type); + if (type == ResponseType::UNKNOWN) { + DVLOG(2) << "Unknown response message type= " << message_type; + return false; + } + + auto* found = raw_value->FindKey("answer"); + if (found && !found->is_none()) { + answer = std::make_unique<Answer>(); + if (!answer->Parse(*found)) + return false; + } + + found = raw_value->FindKey("status"); + if (found && !found->is_none()) { + status = std::make_unique<ReceiverStatus>(); + if (!status->Parse(*found)) + return false; + } + + found = raw_value->FindKey("capabilities"); + if (found && !found->is_none()) { + capabilities = std::make_unique<ReceiverCapability>(); + if (!capabilities->Parse(*found)) + return false; + } + + found = raw_value->FindKey("rpc"); + if (found && !found->is_none()) { + // Decode the base64-encoded string. + if (!found->is_string() || !base::Base64Decode(found->GetString(), &rpc)) + return false; + } + + return true; +} + +} // namespace mirroring diff --git a/chromium/components/mirroring/service/receiver_response.h b/chromium/components/mirroring/service/receiver_response.h new file mode 100644 index 00000000000..0429e2fc6e9 --- /dev/null +++ b/chromium/components/mirroring/service/receiver_response.h @@ -0,0 +1,134 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_MIRRORING_SERVICE_RECEIVER_RESPONSE_H_ +#define COMPONENTS_MIRRORING_SERVICE_RECEIVER_RESPONSE_H_ + +#include <memory> +#include <string> +#include <vector> + +#include "base/values.h" + +namespace mirroring { + +// Receiver response message type. +enum ResponseType { + UNKNOWN, + ANSWER, // Response to OFFER message. + STATUS_RESPONSE, // Response to GET_STATUS message. + CAPABILITIES_RESPONSE, // Response to GET_CAPABILITIES message. + RPC, // Rpc binary messages. The payload is base64 encoded. +}; + +struct Answer { + Answer(); + ~Answer(); + Answer(const Answer& answer); + bool Parse(const base::Value& raw_value); + + // The UDP port used for all streams in this session. + int32_t udp_port; + // The indexes chosen from the OFFER message. + std::vector<int32_t> send_indexes; + // The RTP SSRC used to send the RTCP feedback of the stream, indicated by + // the |send_indexes| above. + std::vector<int32_t> ssrcs; + // A 128bit hex number containing the initialization vector for the crypto. + std::string iv; + // Indicates whether receiver supports the GET_STATUS command. + bool supports_get_status; + // "mirroring" for screen mirroring, or "remoting" for media remoting. + std::string cast_mode; +}; + +struct ReceiverStatus { + ReceiverStatus(); + ~ReceiverStatus(); + ReceiverStatus(const ReceiverStatus& status); + bool Parse(const base::Value& raw_value); + + // Current WiFi signal to noise ratio in decibels. + double wifi_snr; + // Min, max, average, and current bandwidth in bps in order of the WiFi link. + // Example: [1200, 1300, 1250, 1230]. + std::vector<int32_t> wifi_speed; +}; + +struct ReceiverKeySystem { + ReceiverKeySystem(); + ~ReceiverKeySystem(); + ReceiverKeySystem(const ReceiverKeySystem& receiver_key_system); + bool Parse(const base::Value& raw_value); + + // Reverse URI (e.g. com.widevine.alpha). + std::string name; + // EME init data types (e.g. cenc). + std::vector<std::string> init_data_types; + // Codecs supported by key system. This will include AVC and VP8 on all + // Chromecasts. + std::vector<std::string> codecs; + // Codecs that are also hardware-secure. + std::vector<std::string> secure_codecs; + // Support levels for audio encryption robustness. + std::vector<std::string> audio_robustness; + // Support levels for video encryption robustness. + std::vector<std::string> video_robustness; + + std::string persistent_license_session_support; + std::string persistent_release_message_session_support; + std::string persistent_state_support; + std::string distinctive_identifier_support; +}; + +struct ReceiverCapability { + ReceiverCapability(); + ~ReceiverCapability(); + ReceiverCapability(const ReceiverCapability& capabilities); + bool Parse(const base::Value& raw_value); + + // Set of capabilities (e.g., ac3, 4k, hevc, vp9, dolby_vision, etc.). + std::vector<std::string> media_caps; + std::vector<ReceiverKeySystem> key_systems; +}; + +struct ReceiverError { + ReceiverError(); + ~ReceiverError(); + bool Parse(const base::Value& raw_value); + + int32_t code; + std::string description; + std::string details; // In JSON format. +}; + +struct ReceiverResponse { + ReceiverResponse(); + ~ReceiverResponse(); + ReceiverResponse(ReceiverResponse&& receiver_response); + ReceiverResponse& operator=(ReceiverResponse&& receiver_response); + bool Parse(const std::string& message_data); + + ResponseType type; + // All messages have same |session_id| for each mirroring session. This value + // is provided by the media router provider. + int32_t session_id; + // This should be same as the value in the corresponding query/OFFER messages + // for non-rpc messages. + int32_t sequence_number; + + std::string result; // "ok" or "error". + + // Only one of the following has value, according to |type|. + std::unique_ptr<Answer> answer; + std::string rpc; + std::unique_ptr<ReceiverStatus> status; + std::unique_ptr<ReceiverCapability> capabilities; + // Can only be non-null when result is "error". + std::unique_ptr<ReceiverError> error; +}; + +} // namespace mirroring + +#endif // COMPONENTS_MIRRORING_SERVICE_RECEIVER_RESPONSE_H_ diff --git a/chromium/components/mirroring/service/receiver_response_unittest.cc b/chromium/components/mirroring/service/receiver_response_unittest.cc new file mode 100644 index 00000000000..807a683f48d --- /dev/null +++ b/chromium/components/mirroring/service/receiver_response_unittest.cc @@ -0,0 +1,263 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/mirroring/service/receiver_response.h" + +#include "base/base64.h" +#include "base/bind.h" +#include "base/callback.h" +#include "base/json/json_reader.h" +#include "base/macros.h" +#include "base/test/mock_callback.h" +#include "components/mirroring/service/value_util.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using ::testing::InvokeWithoutArgs; +using ::testing::_; + +namespace mirroring { + +class ReceiverResponseTest : public ::testing::Test { + public: + ReceiverResponseTest() {} + ~ReceiverResponseTest() override {} + + private: + DISALLOW_COPY_AND_ASSIGN(ReceiverResponseTest); +}; + +TEST_F(ReceiverResponseTest, ParseValidJson) { + const std::string response_string = "{\"type\":\"ANSWER\",\"result\":\"ok\"}"; + ReceiverResponse response; + ASSERT_TRUE(response.Parse(response_string)); + EXPECT_EQ(-1, response.session_id); + EXPECT_EQ(-1, response.sequence_number); + EXPECT_EQ(ResponseType::ANSWER, response.type); + EXPECT_EQ("ok", response.result); + EXPECT_FALSE(response.answer); + EXPECT_FALSE(response.status); + EXPECT_FALSE(response.capabilities); + EXPECT_FALSE(response.error); + EXPECT_TRUE(response.rpc.empty()); +} + +TEST_F(ReceiverResponseTest, ParseInvalidValueType) { + const std::string response_string = + "{\"sessionId\":123, \"seqNum\":\"one-two-three\"}"; + ReceiverResponse response; + EXPECT_FALSE(response.Parse(response_string)); +} + +TEST_F(ReceiverResponseTest, ParseNonJsonString) { + const std::string response_string = "This is not JSON."; + ReceiverResponse response; + EXPECT_FALSE(response.Parse(response_string)); +} + +TEST_F(ReceiverResponseTest, ParseRealWorldAnswerMessage) { + const std::string response_string = + "{\"answer\":{\"receiverRtcpEventLog\":[0,1],\"rtpExtensions\":" + "[[\"adaptive_playout_delay\"],[\"adaptive_playout_delay\"]]," + "\"sendIndexes\":[0,1],\"ssrcs\":[40863,759293],\"udpPort\":50691," + "\"castMode\":\"mirroring\"},\"result\":\"ok\",\"seqNum\":989031000," + "\"type\":\"ANSWER\"}"; + ReceiverResponse response; + ASSERT_TRUE(response.Parse(response_string)); + EXPECT_EQ(-1, response.session_id); + EXPECT_EQ(989031000, response.sequence_number); + EXPECT_EQ(ResponseType::ANSWER, response.type); + EXPECT_EQ("ok", response.result); + ASSERT_TRUE(response.answer); + EXPECT_EQ(50691, response.answer->udp_port); + EXPECT_EQ(std::vector<int32_t>({0, 1}), response.answer->send_indexes); + EXPECT_EQ(std::vector<int32_t>({40863, 759293}), response.answer->ssrcs); + EXPECT_TRUE(response.answer->iv.empty()); + EXPECT_EQ(false, response.answer->supports_get_status); + EXPECT_EQ("mirroring", response.answer->cast_mode); + EXPECT_FALSE(response.status); + EXPECT_FALSE(response.capabilities); + EXPECT_FALSE(response.error); +} + +TEST_F(ReceiverResponseTest, ParseErrorMessage) { + const std::string response_string = + "{\"sessionId\": 123," + "\"seqNum\": 999," + "\"type\": \"ANSWER\"," + "\"result\": \"error\"," + "\"error\": {" + "\"code\": 42," + "\"description\": \"it is broke\"," + "\"details\": {\"foo\": -1, \"bar\": 88}" + "}" + "}"; + ReceiverResponse response; + ASSERT_TRUE(response.Parse(response_string)); + EXPECT_EQ(123, response.session_id); + EXPECT_EQ(999, response.sequence_number); + EXPECT_EQ(ResponseType::ANSWER, response.type); + EXPECT_EQ("error", response.result); + EXPECT_FALSE(response.answer); + EXPECT_FALSE(response.status); + EXPECT_FALSE(response.capabilities); + ASSERT_TRUE(response.error); + EXPECT_EQ(42, response.error->code); + EXPECT_EQ("it is broke", response.error->description); + std::unique_ptr<base::Value> parsed_details = + base::JSONReader::Read(response.error->details); + ASSERT_TRUE(parsed_details && parsed_details->is_dict()); + EXPECT_EQ(2u, parsed_details->DictSize()); + int fool_value = 0; + EXPECT_TRUE(GetInt(*parsed_details, "foo", &fool_value)); + EXPECT_EQ(-1, fool_value); + int bar_value = 0; + EXPECT_TRUE(GetInt(*parsed_details, "bar", &bar_value)); + EXPECT_EQ(88, bar_value); +} + +TEST_F(ReceiverResponseTest, ParseStatusMessage) { + const std::string response_string = + "{\"seqNum\": 777," + "\"type\": \"STATUS_RESPONSE\"," + "\"result\": \"ok\"," + "\"status\": {" + "\"wifiSnr\": 36.7," + "\"wifiSpeed\": [1234, 5678, 3000, 3001]," + "\"wifiFcsError\": [12, 13, 12, 12]}" // This will be ignored. + "}"; + ReceiverResponse response; + ASSERT_TRUE(response.Parse(response_string)); + EXPECT_EQ(777, response.sequence_number); + EXPECT_EQ(ResponseType::STATUS_RESPONSE, response.type); + EXPECT_EQ("ok", response.result); + EXPECT_FALSE(response.error); + EXPECT_FALSE(response.answer); + ASSERT_TRUE(response.status); + EXPECT_EQ(36.7, response.status->wifi_snr); + const std::vector<int32_t> expect_speed({1234, 5678, 3000, 3001}); + EXPECT_EQ(expect_speed, response.status->wifi_speed); + EXPECT_FALSE(response.capabilities); +} + +TEST_F(ReceiverResponseTest, ParseCapabilityMessage) { + const std::string response_string = + "{\"sessionId\": 999888777," + "\"seqNum\": 5551212," + "\"type\": \"CAPABILITIES_RESPONSE\"," + "\"result\": \"ok\"," + "\"capabilities\": {" + "\"mediaCaps\": [\"audio\", \"video\", \"vp9\"]," + "\"keySystems\": [" + "{" + "\"keySystemName\": \"com.w3c.clearkey\"" + "}," + "{" + "\"keySystemName\": \"com.widevine.alpha\"," + "\"initDataTypes\": [\"a\", \"b\", \"c\", \"1\", \"2\", \"3\"]," + "\"codecs\": [\"vp8\", \"h264\"]," + "\"secureCodecs\": [\"h264\", \"vp8\"]," + "\"audioRobustness\": [\"nope\"]," + "\"videoRobustness\": [\"yep\"]," + "\"persistentLicenseSessionSupport\": \"SUPPORTED\"," + "\"persistentReleaseMessageSessionSupport\": \"SUPPORTED_WITH_ID\"," + "\"persistentStateSupport\": \"REQUESTABLE\"," + "\"distinctiveIdentifierSupport\": \"ALWAYS_ENABLED\"" + "}" + "]}}"; + ReceiverResponse response; + ASSERT_TRUE(response.Parse(response_string)); + EXPECT_EQ(999888777, response.session_id); + EXPECT_EQ(5551212, response.sequence_number); + EXPECT_EQ(ResponseType::CAPABILITIES_RESPONSE, response.type); + EXPECT_EQ("ok", response.result); + EXPECT_FALSE(response.error); + EXPECT_FALSE(response.answer); + EXPECT_FALSE(response.status); + ASSERT_TRUE(response.capabilities); + EXPECT_EQ(std::vector<std::string>({"audio", "video", "vp9"}), + response.capabilities->media_caps); + const ReceiverKeySystem& first_key_system = + response.capabilities->key_systems[0]; + EXPECT_EQ("com.w3c.clearkey", first_key_system.name); + EXPECT_TRUE(first_key_system.init_data_types.empty()); + EXPECT_TRUE(first_key_system.codecs.empty()); + EXPECT_TRUE(first_key_system.secure_codecs.empty()); + EXPECT_TRUE(first_key_system.audio_robustness.empty()); + EXPECT_TRUE(first_key_system.video_robustness.empty()); + EXPECT_TRUE(first_key_system.persistent_license_session_support.empty()); + EXPECT_TRUE( + first_key_system.persistent_release_message_session_support.empty()); + EXPECT_TRUE(first_key_system.persistent_state_support.empty()); + EXPECT_TRUE(first_key_system.distinctive_identifier_support.empty()); + const ReceiverKeySystem& second_key_system = + response.capabilities->key_systems[1]; + EXPECT_EQ("com.widevine.alpha", second_key_system.name); + EXPECT_EQ(std::vector<std::string>({"a", "b", "c", "1", "2", "3"}), + second_key_system.init_data_types); + EXPECT_EQ(std::vector<std::string>({"vp8", "h264"}), + second_key_system.codecs); + EXPECT_EQ(std::vector<std::string>({"h264", "vp8"}), + second_key_system.secure_codecs); + EXPECT_EQ(std::vector<std::string>({"nope"}), + second_key_system.audio_robustness); + EXPECT_EQ(std::vector<std::string>({"yep"}), + second_key_system.video_robustness); + EXPECT_EQ("SUPPORTED", second_key_system.persistent_license_session_support); + EXPECT_EQ("SUPPORTED_WITH_ID", + second_key_system.persistent_release_message_session_support); + EXPECT_EQ("REQUESTABLE", second_key_system.persistent_state_support); + EXPECT_EQ("ALWAYS_ENABLED", second_key_system.distinctive_identifier_support); +} + +TEST_F(ReceiverResponseTest, ParseRpcMessage) { + const std::string message = "Hello from the Cast Receiver!"; + std::string rpc_base64; + base::Base64Encode(message, &rpc_base64); + std::string response_string = + "{\"sessionId\": 735189," + "\"seqNum\": 6789," + "\"type\": \"RPC\"," + "\"result\": \"ok\"," + "\"rpc\": \"" + + rpc_base64 + "\"}"; + ReceiverResponse response; + ASSERT_TRUE(response.Parse(response_string)); + EXPECT_EQ(735189, response.session_id); + EXPECT_EQ(6789, response.sequence_number); + EXPECT_EQ("ok", response.result); + EXPECT_EQ(ResponseType::RPC, response.type); + EXPECT_EQ(message, response.rpc); + EXPECT_FALSE(response.error); + EXPECT_FALSE(response.answer); + EXPECT_FALSE(response.status); + EXPECT_FALSE(response.capabilities); +} + +TEST_F(ReceiverResponseTest, ParseResponseWithNullField) { + const std::string response_string = + "{\"sessionId\":null,\"seqNum\":808907000,\"type\":\"ANSWER\"," + "\"result\":\"ok\",\"rpc\":null,\"error\":null," + "\"answer\":{\"udpPort\":51706,\"sendIndexes\":[0,1]," + "\"ssrcs\":[152818,556029],\"IV\":null,\"receiverGetStatus\":true," + "\"castMode\":\"mirroring\"},\"status\":null,\"capabilities\":null}"; + ReceiverResponse response; + ASSERT_TRUE(response.Parse(response_string)); + EXPECT_EQ(808907000, response.sequence_number); + EXPECT_EQ("ok", response.result); + EXPECT_FALSE(response.error); + EXPECT_FALSE(response.status); + EXPECT_FALSE(response.capabilities); + EXPECT_TRUE(response.rpc.empty()); + EXPECT_EQ(ResponseType::ANSWER, response.type); + ASSERT_TRUE(response.answer); + EXPECT_EQ(51706, response.answer->udp_port); + EXPECT_EQ(std::vector<int32_t>({0, 1}), response.answer->send_indexes); + EXPECT_EQ(std::vector<int32_t>({152818, 556029}), response.answer->ssrcs); + EXPECT_TRUE(response.answer->iv.empty()); + EXPECT_EQ(true, response.answer->supports_get_status); + EXPECT_EQ("mirroring", response.answer->cast_mode); +} + +} // namespace mirroring diff --git a/chromium/components/mirroring/service/rtp_stream.cc b/chromium/components/mirroring/service/rtp_stream.cc index 1051557731c..d9a95d39b28 100644 --- a/chromium/components/mirroring/service/rtp_stream.cc +++ b/chromium/components/mirroring/service/rtp_stream.cc @@ -8,7 +8,6 @@ #include "base/logging.h" #include "base/macros.h" #include "base/values.h" -#include "build/build_config.h" #include "media/base/video_frame.h" #include "media/cast/cast_config.h" #include "media/cast/sender/audio_sender.h" @@ -30,81 +29,6 @@ constexpr base::TimeDelta kRefreshInterval = // limit (60 * 250ms = 15 seconds), refresh frame requests will stop being made. constexpr int kMaxConsecutiveRefreshFrames = 60; -FrameSenderConfig DefaultOpusConfig() { - FrameSenderConfig config; - config.rtp_payload_type = RtpPayloadType::AUDIO_OPUS; - config.sender_ssrc = 1; - config.receiver_ssrc = 2; - config.rtp_timebase = media::cast::kDefaultAudioSamplingRate; - config.channels = 2; - config.min_bitrate = config.max_bitrate = config.start_bitrate = - media::cast::kDefaultAudioEncoderBitrate; - config.max_frame_rate = 100; // 10 ms audio frames - config.codec = media::cast::CODEC_AUDIO_OPUS; - return config; -} - -FrameSenderConfig DefaultVp8Config() { - FrameSenderConfig config; - config.rtp_payload_type = RtpPayloadType::VIDEO_VP8; - config.sender_ssrc = 11; - config.receiver_ssrc = 12; - config.rtp_timebase = media::cast::kVideoFrequency; - config.channels = 1; - config.max_bitrate = media::cast::kDefaultMaxVideoBitrate; - config.min_bitrate = media::cast::kDefaultMinVideoBitrate; - config.max_frame_rate = media::cast::kDefaultMaxFrameRate; - config.codec = media::cast::CODEC_VIDEO_VP8; - return config; -} - -FrameSenderConfig DefaultH264Config() { - FrameSenderConfig config; - config.rtp_payload_type = RtpPayloadType::VIDEO_H264; - config.sender_ssrc = 11; - config.receiver_ssrc = 12; - config.rtp_timebase = media::cast::kVideoFrequency; - config.channels = 1; - config.max_bitrate = media::cast::kDefaultMaxVideoBitrate; - config.min_bitrate = media::cast::kDefaultMinVideoBitrate; - config.max_frame_rate = media::cast::kDefaultMaxFrameRate; - config.codec = media::cast::CODEC_VIDEO_H264; - return config; -} - -bool IsHardwareVP8EncodingSupported(RtpStreamClient* client) { - // Query for hardware VP8 encoder support. - const std::vector<media::VideoEncodeAccelerator::SupportedProfile> - vea_profiles = client->GetSupportedVideoEncodeAcceleratorProfiles(); - for (const auto& vea_profile : vea_profiles) { - if (vea_profile.profile >= media::VP8PROFILE_MIN && - vea_profile.profile <= media::VP8PROFILE_MAX) { - return true; - } - } - return false; -} - -bool IsHardwareH264EncodingSupported(RtpStreamClient* client) { -// Query for hardware H.264 encoder support. -// -// TODO(miu): Look into why H.264 hardware encoder on MacOS is broken. -// http://crbug.com/596674 -// TODO(emircan): Look into HW encoder initialization issues on Win. -// https://crbug.com/636064 -#if !defined(OS_MACOSX) && !defined(OS_WIN) - const std::vector<media::VideoEncodeAccelerator::SupportedProfile> - vea_profiles = client->GetSupportedVideoEncodeAcceleratorProfiles(); - for (const auto& vea_profile : vea_profiles) { - if (vea_profile.profile >= media::H264PROFILE_MIN && - vea_profile.profile <= media::H264PROFILE_MAX) { - return true; - } - } -#endif // !defined(OS_MACOSX) && !defined(OS_WIN) - return false; -} - } // namespace VideoRtpStream::VideoRtpStream( @@ -125,24 +49,6 @@ VideoRtpStream::VideoRtpStream( VideoRtpStream::~VideoRtpStream() {} -// static -std::vector<FrameSenderConfig> VideoRtpStream::GetSupportedConfigs( - RtpStreamClient* client) { - std::vector<FrameSenderConfig> supported_configs; - // Prefer VP8 over H.264 for hardware encoder. - if (IsHardwareVP8EncodingSupported(client)) - supported_configs.push_back(DefaultVp8Config()); - if (IsHardwareH264EncodingSupported(client)) - supported_configs.push_back(DefaultH264Config()); - - // Propose the default software VP8 encoder, if no hardware encoders are - // available. - if (supported_configs.empty()) - supported_configs.push_back(DefaultVp8Config()); - - return supported_configs; -} - void VideoRtpStream::InsertVideoFrame( scoped_refptr<media::VideoFrame> video_frame) { DCHECK(client_); @@ -205,11 +111,6 @@ AudioRtpStream::AudioRtpStream( AudioRtpStream::~AudioRtpStream() {} -// static -std::vector<FrameSenderConfig> AudioRtpStream::GetSupportedConfigs() { - return {DefaultOpusConfig()}; -} - void AudioRtpStream::InsertAudio(std::unique_ptr<media::AudioBus> audio_bus, base::TimeTicks capture_time) { audio_sender_->InsertAudio(std::move(audio_bus), capture_time); diff --git a/chromium/components/mirroring/service/rtp_stream.h b/chromium/components/mirroring/service/rtp_stream.h index ef5788d953c..9bffae1f60d 100644 --- a/chromium/components/mirroring/service/rtp_stream.h +++ b/chromium/components/mirroring/service/rtp_stream.h @@ -44,10 +44,6 @@ class RtpStreamClient { // The following are for hardware video encoding. - // Query the supported hardware encoding profiles. - virtual media::VideoEncodeAccelerator::SupportedProfiles - GetSupportedVideoEncodeAcceleratorProfiles() = 0; - virtual void CreateVideoEncodeAccelerator( const media::cast::ReceiveVideoEncodeAcceleratorCallback& callback) = 0; @@ -71,9 +67,6 @@ class VideoRtpStream { base::WeakPtr<RtpStreamClient> client); ~VideoRtpStream(); - static std::vector<media::cast::FrameSenderConfig> GetSupportedConfigs( - RtpStreamClient* client); - // Called by VideoCaptureClient when a video frame is received. // |video_frame| is required to provide REFERENCE_TIME in the metadata. void InsertVideoFrame(scoped_refptr<media::VideoFrame> video_frame); @@ -115,8 +108,6 @@ class AudioRtpStream { base::WeakPtr<RtpStreamClient> client); ~AudioRtpStream(); - static std::vector<media::cast::FrameSenderConfig> GetSupportedConfigs(); - // Called by AudioCaptureClient when new audio data is available. void InsertAudio(std::unique_ptr<media::AudioBus> audio_bus, base::TimeTicks estimated_capture_time); diff --git a/chromium/components/mirroring/service/rtp_stream_unittest.cc b/chromium/components/mirroring/service/rtp_stream_unittest.cc index 1a2d2283643..b0a0ae26b04 100644 --- a/chromium/components/mirroring/service/rtp_stream_unittest.cc +++ b/chromium/components/mirroring/service/rtp_stream_unittest.cc @@ -43,10 +43,6 @@ class DummyClient final : public RtpStreamClient { void CreateVideoEncodeMemory( size_t size, const media::cast::ReceiveVideoEncodeMemoryCallback& callback) override {} - media::VideoEncodeAccelerator::SupportedProfiles - GetSupportedVideoEncodeAcceleratorProfiles() override { - return media::VideoEncodeAccelerator::SupportedProfiles(); - } base::WeakPtr<RtpStreamClient> GetWeakPtr() { return weak_factory_.GetWeakPtr(); diff --git a/chromium/components/mirroring/service/session.cc b/chromium/components/mirroring/service/session.cc index 1d97af99207..58047f8db81 100644 --- a/chromium/components/mirroring/service/session.cc +++ b/chromium/components/mirroring/service/session.cc @@ -4,23 +4,34 @@ #include "components/mirroring/service/session.h" +#include "base/json/json_writer.h" #include "base/logging.h" +#include "base/rand_util.h" +#include "base/stl_util.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_util.h" +#include "base/sys_info.h" #include "base/task_scheduler/post_task.h" #include "base/threading/thread_task_runner_handle.h" #include "base/time/default_tick_clock.h" #include "base/time/time.h" +#include "base/values.h" +#include "build/build_config.h" #include "components/mirroring/service/udp_socket_client.h" #include "components/mirroring/service/video_capture_client.h" +#include "crypto/random.h" #include "media/cast/net/cast_transport.h" #include "media/cast/sender/audio_sender.h" #include "media/cast/sender/video_sender.h" #include "media/video/video_encode_accelerator.h" #include "mojo/public/cpp/bindings/binding.h" #include "mojo/public/cpp/system/platform_handle.h" +#include "net/base/ip_endpoint.h" using media::cast::FrameSenderConfig; using media::cast::RtpPayloadType; using media::cast::CastTransportStatus; +using media::cast::Codec; using media::cast::FrameEvent; using media::cast::PacketEvent; using media::cast::OperationalStatus; @@ -39,6 +50,23 @@ constexpr base::TimeDelta kSendEventsInterval = base::TimeDelta::FromSeconds(1); constexpr base::TimeDelta kOfferAnswerExchangeTimeout = base::TimeDelta::FromSeconds(15); +// Used for OFFER/ANSWER message exchange. Some receivers will error out on +// payloadType values other than the ones hard-coded here. +constexpr int kAudioPayloadType = 127; +constexpr int kVideoPayloadType = 96; + +constexpr int kAudioSsrcMin = 1; +constexpr int kAudioSsrcMax = 5e5; +constexpr int kVideoSsrcMin = 5e5 + 1; +constexpr int kVideoSsrcMax = 10e5; + +// Maximum number of bytes of file data allowed in a single Crash report. As of +// this writing, the total report upload size is capped at 20 MB. +// +// 2 KB of "overhead bytes" are subtracted to account for all of the non-file +// data in a report upload, including HTTP headers/requests and form data. +constexpr int kMaxCrashReportBytes = (20 * 1024 - 2) * 1024; + class TransportClient final : public media::cast::CastTransport::Client { public: explicit TransportClient(Session* session) : session_(session) {} @@ -67,142 +95,192 @@ class TransportClient final : public media::cast::CastTransport::Client { DISALLOW_COPY_AND_ASSIGN(TransportClient); }; -} // namespace - -Session::Session(SessionType session_type, - const net::IPEndPoint& receiver_endpoint, - SessionClient* client) - : client_(client), weak_factory_(this) { - DCHECK(client_); - - std::vector<FrameSenderConfig> audio_configs; - std::vector<FrameSenderConfig> video_configs; - if (session_type != SessionType::VIDEO_ONLY) - audio_configs = AudioRtpStream::GetSupportedConfigs(); - if (session_type != SessionType::AUDIO_ONLY) - video_configs = VideoRtpStream::GetSupportedConfigs(this); - start_timeout_timer_.Start( - FROM_HERE, kOfferAnswerExchangeTimeout, - base::BindRepeating(&Session::OnOfferAnswerExchangeTimeout, - weak_factory_.GetWeakPtr())); - client_->DoOfferAnswerExchange( - audio_configs, video_configs, - base::BindOnce(&Session::StartInternal, weak_factory_.GetWeakPtr(), - receiver_endpoint)); +// Generates a string with cryptographically secure random bytes. +std::string MakeRandomString(size_t length) { + std::string result(length, ' '); + crypto::RandBytes(base::data(result), length); + return result; } -Session::~Session() { - StopSession(); +int NumberOfEncodeThreads() { + // Do not saturate CPU utilization just for encoding. On a lower-end system + // with only 1 or 2 cores, use only one thread for encoding. On systems with + // more cores, allow half of the cores to be used for encoding. + return std::min(8, (base::SysInfo::NumberOfProcessors() + 1) / 2); } -void Session::StartInternal(const net::IPEndPoint& receiver_endpoint, - const FrameSenderConfig& audio_config, - const FrameSenderConfig& video_config) { - DVLOG(1) << __func__; - start_timeout_timer_.Stop(); - - DCHECK(!video_capture_client_); - DCHECK(!cast_transport_); - DCHECK(!audio_stream_); - DCHECK(!video_stream_); - DCHECK(!cast_environment_); - DCHECK(client_); - - if (audio_config.rtp_payload_type == RtpPayloadType::REMOTE_AUDIO || - video_config.rtp_payload_type == RtpPayloadType::REMOTE_VIDEO) { - NOTIMPLEMENTED(); // TODO(xjz): Add support for media remoting. - return; +// Scan profiles for hardware VP8 encoder support. +bool IsHardwareVP8EncodingSupported( + const std::vector<media::VideoEncodeAccelerator::SupportedProfile>& + profiles) { + for (const auto& vea_profile : profiles) { + if (vea_profile.profile >= media::VP8PROFILE_MIN && + vea_profile.profile <= media::VP8PROFILE_MAX) { + return true; + } } + return false; +} - const bool has_audio = - (audio_config.rtp_payload_type < RtpPayloadType::AUDIO_LAST) && - (audio_config.rtp_payload_type >= RtpPayloadType::FIRST); - const bool has_video = - (video_config.rtp_payload_type > RtpPayloadType::AUDIO_LAST) && - (video_config.rtp_payload_type < RtpPayloadType::LAST); - if (!has_audio && !has_video) { - VLOG(1) << "Incorrect ANSWER message: No audio or Video."; - client_->OnError(SESSION_START_ERROR); - return; +// Scan profiles for hardware H.264 encoder support. +bool IsHardwareH264EncodingSupported( + const std::vector<media::VideoEncodeAccelerator::SupportedProfile>& + profiles) { +// TODO(miu): Look into why H.264 hardware encoder on MacOS is broken. +// http://crbug.com/596674 +// TODO(emircan): Look into HW encoder initialization issues on Win. +// https://crbug.com/636064 +#if !defined(OS_MACOSX) && !defined(OS_WIN) + for (const auto& vea_profile : profiles) { + if (vea_profile.profile >= media::H264PROFILE_MIN && + vea_profile.profile <= media::H264PROFILE_MAX) { + return true; + } } +#endif // !defined(OS_MACOSX) && !defined(OS_WIN) + return false; +} - audio_encode_thread_ = base::CreateSingleThreadTaskRunnerWithTraits( - {base::TaskPriority::USER_BLOCKING, - base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN}, - base::SingleThreadTaskRunnerThreadMode::DEDICATED); - video_encode_thread_ = base::CreateSingleThreadTaskRunnerWithTraits( - {base::TaskPriority::USER_BLOCKING, - base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN}, - base::SingleThreadTaskRunnerThreadMode::DEDICATED); - cast_environment_ = new media::cast::CastEnvironment( - base::DefaultTickClock::GetInstance(), - base::ThreadTaskRunnerHandle::Get(), audio_encode_thread_, - video_encode_thread_); - network::mojom::NetworkContextPtr network_context; - client_->GetNewWorkContext(mojo::MakeRequest(&network_context)); - auto udp_client = std::make_unique<UdpSocketClient>( - receiver_endpoint, std::move(network_context), - base::BindOnce(&Session::ReportError, weak_factory_.GetWeakPtr(), - SessionError::CAST_TRANSPORT_ERROR)); - cast_transport_ = media::cast::CastTransport::Create( - cast_environment_->Clock(), kSendEventsInterval, - std::make_unique<TransportClient>(this), std::move(udp_client), - base::ThreadTaskRunnerHandle::Get()); +// Helper to add |config| to |config_list| with given |aes_key|. +void AddSenderConfig(int32_t sender_ssrc, + FrameSenderConfig config, + const std::string& aes_key, + const std::string& aes_iv, + std::vector<FrameSenderConfig>* config_list) { + config.aes_key = aes_key; + config.aes_iv_mask = aes_iv; + config.sender_ssrc = sender_ssrc; + config_list->emplace_back(config); +} - if (has_audio) { - auto audio_sender = std::make_unique<media::cast::AudioSender>( - cast_environment_, audio_config, - base::BindRepeating(&Session::OnEncoderStatusChange, - weak_factory_.GetWeakPtr()), - cast_transport_.get()); - audio_stream_ = std::make_unique<AudioRtpStream>( - std::move(audio_sender), weak_factory_.GetWeakPtr()); - // TODO(xjz): Start audio capturing. - NOTIMPLEMENTED(); +// Generate the stream object from |config| and add it to |stream_list|. +void AddStreamObject(int stream_index, + const std::string& codec_name, + const FrameSenderConfig& config, + const MirrorSettings& mirror_settings, + base::Value::ListStorage* stream_list) { + base::Value stream(base::Value::Type::DICTIONARY); + stream.SetKey("index", base::Value(stream_index)); + stream.SetKey("codecName", base::Value(base::ToLowerASCII(codec_name))); + stream.SetKey("rtpProfile", base::Value("cast")); + const bool is_audio = + (config.rtp_payload_type <= media::cast::RtpPayloadType::AUDIO_LAST); + stream.SetKey("rtpPayloadType", + base::Value(is_audio ? kAudioPayloadType : kVideoPayloadType)); + stream.SetKey("ssrc", base::Value(int(config.sender_ssrc))); + stream.SetKey( + "targetDelay", + base::Value(int(config.animated_playout_delay.InMilliseconds()))); + stream.SetKey("aesKey", base::Value(base::HexEncode(config.aes_key.data(), + config.aes_key.size()))); + stream.SetKey("aesIvMask", + base::Value(base::HexEncode(config.aes_iv_mask.data(), + config.aes_iv_mask.size()))); + stream.SetKey("timeBase", + base::Value("1/" + std::to_string(config.rtp_timebase))); + stream.SetKey("receiverRtcpEventLog", base::Value(true)); + stream.SetKey("rtpExtensions", base::Value("adaptive_playout_delay")); + if (is_audio) { + // Note on "AUTO" bitrate calculation: This is based on libopus source + // at the time of this writing. Internally, it uses the following math: + // + // packet_overhead_bps = 60 bits * num_packets_in_one_second + // approx_encoded_signal_bps = frequency * channels + // estimated_bps = packet_overhead_bps + approx_encoded_signal_bps + // + // For 100 packets/sec at 48 kHz and 2 channels, this is 102kbps. + const int bitrate = config.max_bitrate > 0 + ? config.max_bitrate + : (60 * config.max_frame_rate + + config.rtp_timebase * config.channels); + stream.SetKey("type", base::Value("audio_source")); + stream.SetKey("bitRate", base::Value(bitrate)); + stream.SetKey("sampleRate", base::Value(config.rtp_timebase)); + stream.SetKey("channels", base::Value(config.channels)); + } else /* is video */ { + stream.SetKey("type", base::Value("video_source")); + stream.SetKey("renderMode", base::Value("video")); + stream.SetKey("maxFrameRate", + base::Value(std::to_string(static_cast<int>( + config.max_frame_rate * 1000)) + + "/1000")); + stream.SetKey("maxBitRate", base::Value(config.max_bitrate)); + base::Value::ListStorage resolutions; + base::Value resolution(base::Value::Type::DICTIONARY); + resolution.SetKey("width", base::Value(mirror_settings.max_width())); + resolution.SetKey("height", base::Value(mirror_settings.max_height())); + resolutions.emplace_back(std::move(resolution)); + stream.SetKey("resolutions", base::Value(resolutions)); } + stream_list->emplace_back(std::move(stream)); +} - if (has_video) { - auto video_sender = std::make_unique<media::cast::VideoSender>( - cast_environment_, video_config, - base::BindRepeating(&Session::OnEncoderStatusChange, - weak_factory_.GetWeakPtr()), - base::BindRepeating(&Session::CreateVideoEncodeAccelerator, - weak_factory_.GetWeakPtr()), - base::BindRepeating(&Session::CreateVideoEncodeMemory, - weak_factory_.GetWeakPtr()), - cast_transport_.get(), - base::BindRepeating(&Session::SetTargetPlayoutDelay, - weak_factory_.GetWeakPtr())); - video_stream_ = std::make_unique<VideoRtpStream>( - std::move(video_sender), weak_factory_.GetWeakPtr()); - media::mojom::VideoCaptureHostPtr video_host; - client_->GetVideoCaptureHost(mojo::MakeRequest(&video_host)); - video_capture_client_ = - std::make_unique<VideoCaptureClient>(std::move(video_host)); - video_capture_client_->Start( - base::BindRepeating(&VideoRtpStream::InsertVideoFrame, - video_stream_->AsWeakPtr()), - base::BindOnce(&Session::ReportError, weak_factory_.GetWeakPtr(), - SessionError::VIDEO_CAPTURE_ERROR)); - } +} // namespace + +Session::Session(int32_t session_id, + const CastSinkInfo& sink_info, + const gfx::Size& max_resolution, + SessionObserver* observer, + ResourceProvider* resource_provider, + CastMessageChannel* outbound_channel) + : session_id_(session_id), + sink_info_(sink_info), + observer_(observer), + resource_provider_(resource_provider), + message_dispatcher_(outbound_channel, + base::BindRepeating(&Session::OnResponseParsingError, + base::Unretained(this))), + weak_factory_(this) { + DCHECK(resource_provider_); + mirror_settings_.SetResolutionContraints(max_resolution.width(), + max_resolution.height()); + resource_provider_->GetNetworkContext(mojo::MakeRequest(&network_context_)); + + auto wifi_status_monitor = + std::make_unique<WifiStatusMonitor>(session_id_, &message_dispatcher_); + network::mojom::URLLoaderFactoryPtr url_loader_factory; + network_context_->CreateURLLoaderFactory( + mojo::MakeRequest(&url_loader_factory), + network::mojom::URLLoaderFactoryParams::New( + network::mojom::kBrowserProcessId, false, std::string())); + + // Generate session level tags. + base::Value session_tags(base::Value::Type::DICTIONARY); + session_tags.SetKey("mirrorSettings", mirror_settings_.ToDictionaryValue()); + session_tags.SetKey("shouldCaptureAudio", + base::Value(sink_info_.capability != VIDEO_ONLY)); + session_tags.SetKey("shouldCaptureVideo", + base::Value(sink_info_.capability != AUDIO_ONLY)); + session_tags.SetKey("receiverProductName", + base::Value(sink_info_.model_name)); + + session_monitor_.emplace( + kMaxCrashReportBytes, sink_info_.ip_address, std::move(session_tags), + std::move(url_loader_factory), std::move(wifi_status_monitor)); + + CreateAndSendOffer(); +} - client_->DidStart(); +Session::~Session() { + StopSession(); } void Session::ReportError(SessionError error) { - DVLOG(1) << __func__ << ": error=" << error; - if (client_) - client_->OnError(error); + if (session_monitor_.has_value()) + session_monitor_->OnStreamingError(error); + if (observer_) + observer_->OnError(error); StopSession(); } void Session::StopSession() { DVLOG(1) << __func__; - if (!client_) + if (!resource_provider_) return; + session_monitor_->StopStreamingSession(); + session_monitor_.reset(); weak_factory_.InvalidateWeakPtrs(); - start_timeout_timer_.Stop(); audio_encode_thread_ = nullptr; video_encode_thread_ = nullptr; video_capture_client_.reset(); @@ -210,13 +288,15 @@ void Session::StopSession() { video_stream_.reset(); cast_transport_.reset(); cast_environment_ = nullptr; - client_->DidStop(); - client_ = nullptr; + resource_provider_ = nullptr; + if (observer_) { + observer_->DidStop(); + observer_ = nullptr; + } } void Session::OnError(const std::string& message) { - VLOG(1) << message; - ReportError(SessionError::CAST_STREAMING_ERROR); + ReportError(SessionError::RTP_STREAM_ERROR); } void Session::RequestRefreshFrame() { @@ -238,14 +318,13 @@ void Session::OnEncoderStatusChange(OperationalStatus status) { case OperationalStatus::STATUS_UNSUPPORTED_CODEC: case OperationalStatus::STATUS_CODEC_INIT_FAILED: case OperationalStatus::STATUS_CODEC_RUNTIME_ERROR: - DVLOG(1) << "OperationalStatus error."; - ReportError(SessionError::CAST_STREAMING_ERROR); + ReportError(SessionError::ENCODING_ERROR); break; } } media::VideoEncodeAccelerator::SupportedProfiles -Session::GetSupportedVideoEncodeAcceleratorProfiles() { +Session::GetSupportedVeaProfiles() { // TODO(xjz): Establish GPU channel and query for the supported profiles. return media::VideoEncodeAccelerator::SupportedProfiles(); } @@ -290,13 +369,7 @@ void Session::OnTransportStatusChanged(CastTransportStatus status) { case CastTransportStatus::TRANSPORT_STREAM_INITIALIZED: return; // Not errors, do nothing. case CastTransportStatus::TRANSPORT_INVALID_CRYPTO_CONFIG: - DVLOG(1) << "Warning: unexpected status: " - << "TRANSPORT_INVALID_CRYPTO_CONFIG"; - ReportError(SessionError::CAST_TRANSPORT_ERROR); - break; case CastTransportStatus::TRANSPORT_SOCKET_ERROR: - DVLOG(1) << "Warning: unexpected status: " - << "TRANSPORT_SOCKET_ERROR"; ReportError(SessionError::CAST_TRANSPORT_ERROR); break; } @@ -310,6 +383,156 @@ void Session::OnLoggingEventsReceived( std::move(packet_events)); } +void Session::OnAnswer(const std::string& cast_mode, + const std::vector<FrameSenderConfig>& audio_configs, + const std::vector<FrameSenderConfig>& video_configs, + const ReceiverResponse& response) { + if (!response.answer || response.type == ResponseType::UNKNOWN) { + ReportError(ANSWER_TIME_OUT); + return; + } + + DCHECK_EQ(ResponseType::ANSWER, response.type); + + if (response.result != "ok") { + ReportError(ANSWER_NOT_OK); + return; + } + + const Answer& answer = *response.answer; + if (answer.cast_mode != cast_mode) { + ReportError(ANSWER_MISMATCHED_CAST_MODE); + return; + } + + if (answer.send_indexes.size() != answer.ssrcs.size()) { + ReportError(ANSWER_MISMATCHED_SSRC_LENGTH); + return; + } + + // Select Audio/Video config from ANSWER. + bool has_audio = false; + bool has_video = false; + FrameSenderConfig audio_config; + FrameSenderConfig video_config; + const int video_start_idx = audio_configs.size(); + const int video_idx_bound = video_configs.size() + video_start_idx; + for (size_t i = 0; i < answer.send_indexes.size(); ++i) { + if (answer.send_indexes[i] < 0 || + answer.send_indexes[i] >= video_idx_bound) { + ReportError(ANSWER_SELECT_INVALID_INDEX); + return; + } + if (answer.send_indexes[i] < video_start_idx) { + // Audio + if (has_audio) { + ReportError(ANSWER_SELECT_MULTIPLE_AUDIO); + return; + } + audio_config = audio_configs[answer.send_indexes[i]]; + audio_config.receiver_ssrc = answer.ssrcs[i]; + has_audio = true; + } else { + // Video + if (has_video) { + ReportError(ANSWER_SELECT_MULTIPLE_VIDEO); + return; + } + video_config = video_configs[answer.send_indexes[i] - video_start_idx]; + video_config.receiver_ssrc = answer.ssrcs[i]; + video_config.video_codec_params.number_of_encode_threads = + NumberOfEncodeThreads(); + has_video = true; + } + } + if (!has_audio && !has_video) { + ReportError(ANSWER_NO_AUDIO_OR_VIDEO); + return; + } + + if ((has_audio && + audio_config.rtp_payload_type == RtpPayloadType::REMOTE_AUDIO) || + (has_video && + video_config.rtp_payload_type == RtpPayloadType::REMOTE_VIDEO)) { + NOTIMPLEMENTED(); // TODO(xjz): Add support for media remoting. + return; + } + + // Start streaming. + audio_encode_thread_ = base::CreateSingleThreadTaskRunnerWithTraits( + {base::TaskPriority::USER_BLOCKING, + base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN}, + base::SingleThreadTaskRunnerThreadMode::DEDICATED); + video_encode_thread_ = base::CreateSingleThreadTaskRunnerWithTraits( + {base::TaskPriority::USER_BLOCKING, + base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN}, + base::SingleThreadTaskRunnerThreadMode::DEDICATED); + cast_environment_ = new media::cast::CastEnvironment( + base::DefaultTickClock::GetInstance(), + base::ThreadTaskRunnerHandle::Get(), audio_encode_thread_, + video_encode_thread_); + auto udp_client = std::make_unique<UdpSocketClient>( + net::IPEndPoint(sink_info_.ip_address, answer.udp_port), + network_context_.get(), + base::BindOnce(&Session::ReportError, weak_factory_.GetWeakPtr(), + SessionError::CAST_TRANSPORT_ERROR)); + cast_transport_ = media::cast::CastTransport::Create( + cast_environment_->Clock(), kSendEventsInterval, + std::make_unique<TransportClient>(this), std::move(udp_client), + base::ThreadTaskRunnerHandle::Get()); + + if (has_audio) { + auto audio_sender = std::make_unique<media::cast::AudioSender>( + cast_environment_, audio_config, + base::BindRepeating(&Session::OnEncoderStatusChange, + weak_factory_.GetWeakPtr()), + cast_transport_.get()); + audio_stream_ = std::make_unique<AudioRtpStream>( + std::move(audio_sender), weak_factory_.GetWeakPtr()); + // TODO(xjz): Start audio capturing. + NOTIMPLEMENTED(); + } + + if (has_video) { + auto video_sender = std::make_unique<media::cast::VideoSender>( + cast_environment_, video_config, + base::BindRepeating(&Session::OnEncoderStatusChange, + weak_factory_.GetWeakPtr()), + base::BindRepeating(&Session::CreateVideoEncodeAccelerator, + weak_factory_.GetWeakPtr()), + base::BindRepeating(&Session::CreateVideoEncodeMemory, + weak_factory_.GetWeakPtr()), + cast_transport_.get(), + base::BindRepeating(&Session::SetTargetPlayoutDelay, + weak_factory_.GetWeakPtr())); + video_stream_ = std::make_unique<VideoRtpStream>( + std::move(video_sender), weak_factory_.GetWeakPtr()); + media::mojom::VideoCaptureHostPtr video_host; + resource_provider_->GetVideoCaptureHost(mojo::MakeRequest(&video_host)); + video_capture_client_ = std::make_unique<VideoCaptureClient>( + mirror_settings_.GetVideoCaptureParams(), std::move(video_host)); + video_capture_client_->Start( + base::BindRepeating(&VideoRtpStream::InsertVideoFrame, + video_stream_->AsWeakPtr()), + base::BindOnce(&Session::ReportError, weak_factory_.GetWeakPtr(), + SessionError::VIDEO_CAPTURE_ERROR)); + } + + const SessionMonitor::SessionType session_type = + (has_audio && has_video) + ? SessionMonitor::AUDIO_AND_VIDEO + : has_audio ? SessionMonitor::AUDIO_ONLY : SessionMonitor::VIDEO_ONLY; + session_monitor_->StartStreamingSession(cast_environment_, session_type, + false /* is_remoting */); + + if (observer_) + observer_->DidStart(); +} + +void Session::OnResponseParsingError(const std::string& error_message) { + // TODO(xjz): Log the |error_message| in the mirroring logs. +} + void Session::SetTargetPlayoutDelay(base::TimeDelta playout_delay) { if (audio_stream_) audio_stream_->SetTargetPlayoutDelay(playout_delay); @@ -317,10 +540,78 @@ void Session::SetTargetPlayoutDelay(base::TimeDelta playout_delay) { video_stream_->SetTargetPlayoutDelay(playout_delay); } -void Session::OnOfferAnswerExchangeTimeout() { - VLOG(1) << "OFFER/ANSWER exchange timed out."; - DCHECK(client_); - client_->OnError(SESSION_START_ERROR); +void Session::CreateAndSendOffer() { + // The random AES key and initialization vector pair used by all streams in + // this session. + const std::string aes_key = MakeRandomString(16); // AES-128. + const std::string aes_iv = MakeRandomString(16); // AES has 128-bit blocks. + std::vector<FrameSenderConfig> audio_configs; + std::vector<FrameSenderConfig> video_configs; + + // Generate stream list with supported audio / video configs. + base::Value::ListStorage stream_list; + int stream_index = 0; + if (sink_info_.capability != DeviceCapability::VIDEO_ONLY) { + FrameSenderConfig config = MirrorSettings::GetDefaultAudioConfig( + RtpPayloadType::AUDIO_OPUS, Codec::CODEC_AUDIO_OPUS); + AddSenderConfig(base::RandInt(kAudioSsrcMin, kAudioSsrcMax), config, + aes_key, aes_iv, &audio_configs); + AddStreamObject(stream_index++, "OPUS", audio_configs.back(), + mirror_settings_, &stream_list); + } + if (sink_info_.capability != DeviceCapability::AUDIO_ONLY) { + const int32_t video_ssrc = base::RandInt(kVideoSsrcMin, kVideoSsrcMax); + if (IsHardwareVP8EncodingSupported(GetSupportedVeaProfiles())) { + FrameSenderConfig config = MirrorSettings::GetDefaultVideoConfig( + RtpPayloadType::VIDEO_VP8, Codec::CODEC_VIDEO_VP8); + config.use_external_encoder = true; + AddSenderConfig(video_ssrc, config, aes_key, aes_iv, &video_configs); + AddStreamObject(stream_index++, "VP8", video_configs.back(), + mirror_settings_, &stream_list); + } + if (IsHardwareH264EncodingSupported(GetSupportedVeaProfiles())) { + FrameSenderConfig config = MirrorSettings::GetDefaultVideoConfig( + RtpPayloadType::VIDEO_H264, Codec::CODEC_VIDEO_H264); + config.use_external_encoder = true; + AddSenderConfig(video_ssrc, config, aes_key, aes_iv, &video_configs); + AddStreamObject(stream_index++, "H264", video_configs.back(), + mirror_settings_, &stream_list); + } + if (video_configs.empty()) { + FrameSenderConfig config = MirrorSettings::GetDefaultVideoConfig( + RtpPayloadType::VIDEO_VP8, Codec::CODEC_VIDEO_VP8); + AddSenderConfig(video_ssrc, config, aes_key, aes_iv, &video_configs); + AddStreamObject(stream_index++, "VP8", video_configs.back(), + mirror_settings_, &stream_list); + } + } + DCHECK(!audio_configs.empty() || !video_configs.empty()); + + // Assemble the OFFER message. + const std::string cast_mode = "mirroring"; + base::Value offer(base::Value::Type::DICTIONARY); + offer.SetKey("castMode", base::Value(cast_mode)); + offer.SetKey("receiverGetStatus", base::Value("true")); + offer.SetKey("supportedStreams", base::Value(stream_list)); + + const int32_t sequence_number = message_dispatcher_.GetNextSeqNumber(); + base::Value offer_message(base::Value::Type::DICTIONARY); + offer_message.SetKey("type", base::Value("OFFER")); + offer_message.SetKey("sessionId", base::Value(session_id_)); + offer_message.SetKey("seqNum", base::Value(sequence_number)); + offer_message.SetKey("offer", std::move(offer)); + + CastMessage message_to_receiver; + message_to_receiver.message_namespace = kWebRtcNamespace; + const bool did_serialize_offer = base::JSONWriter::Write( + offer_message, &message_to_receiver.json_format_data); + DCHECK(did_serialize_offer); + + message_dispatcher_.RequestReply( + message_to_receiver, ResponseType::ANSWER, sequence_number, + kOfferAnswerExchangeTimeout, + base::BindOnce(&Session::OnAnswer, base::Unretained(this), cast_mode, + audio_configs, video_configs)); } } // namespace mirroring diff --git a/chromium/components/mirroring/service/session.h b/chromium/components/mirroring/service/session.h index 00cf32b7a22..efc7d8b125b 100644 --- a/chromium/components/mirroring/service/session.h +++ b/chromium/components/mirroring/service/session.h @@ -6,9 +6,14 @@ #define COMPONENTS_MIRRORING_SERVICE_SESSION_H_ #include "base/memory/weak_ptr.h" +#include "base/optional.h" #include "base/single_thread_task_runner.h" #include "components/mirroring/service/interface.h" +#include "components/mirroring/service/message_dispatcher.h" +#include "components/mirroring/service/mirror_settings.h" #include "components/mirroring/service/rtp_stream.h" +#include "components/mirroring/service/session_monitor.h" +#include "components/mirroring/service/wifi_status_monitor.h" #include "media/cast/cast_environment.h" #include "media/cast/net/cast_transport_defines.h" @@ -22,20 +27,32 @@ class CastTransport; namespace mirroring { +struct ReceiverResponse; class VideoCaptureClient; - +class SessionMonitor; + +// Controls a mirroring session, including audio/video capturing and Cast +// Streaming. When constructed, it does OFFER/ANSWER exchange with the mirroring +// receiver. Mirroring starts when the exchange succeeds and stops when this +// class is destructed or error occurs. |observer| will get notified when status +// changes. |outbound_channel| is responsible for sending messages to the +// mirroring receiver through Cast Channel. class Session final : public RtpStreamClient { public: - Session(SessionType session_type, - const net::IPEndPoint& receiver_endpoint, - SessionClient* client); + Session(int32_t session_id, + const CastSinkInfo& sink_info, + const gfx::Size& max_resolution, + SessionObserver* observer, + ResourceProvider* resource_provider, + CastMessageChannel* outbound_channel); + // TODO(xjz): Add mojom::CastMessageChannelRequest |inbound_channel| to + // receive inbound messages. + ~Session() override; // RtpStreamClient implemenation. void OnError(const std::string& message) override; void RequestRefreshFrame() override; - media::VideoEncodeAccelerator::SupportedProfiles - GetSupportedVideoEncodeAcceleratorProfiles() override; void CreateVideoEncodeAccelerator( const media::cast::ReceiveVideoEncodeAcceleratorCallback& callback) override; @@ -49,16 +66,23 @@ class Session final : public RtpStreamClient { std::unique_ptr<std::vector<media::cast::FrameEvent>> frame_events, std::unique_ptr<std::vector<media::cast::PacketEvent>> packet_events); - private: - // Callback when OFFER/ANSWER message exchange finishes. Starts a mirroing - // session. - void StartInternal(const net::IPEndPoint& receiver_endpoint, - const media::cast::FrameSenderConfig& audio_config, - const media::cast::FrameSenderConfig& video_config); + // Callback for ANSWER response. If the ANSWER is invalid, |observer_| will + // get notified with error, and session is stopped. Otherwise, capturing and + // streaming are started with the selected configs. + void OnAnswer( + const std::string& cast_mode, + const std::vector<media::cast::FrameSenderConfig>& audio_configs, + const std::vector<media::cast::FrameSenderConfig>& video_configs, + const ReceiverResponse& response); + // Called by |message_dispatcher_| when error occurs while parsing the + // responses. + void OnResponseParsingError(const std::string& error_message); + + private: void StopSession(); - // Notify |client_| that error occurred and close the session. + // Notify |observer_| that error occurred and close the session. void ReportError(SessionError error); // Callback by Audio/VideoSender to indicate encoder status change. @@ -67,12 +91,26 @@ class Session final : public RtpStreamClient { // Callback by media::cast::VideoSender to set a new target playout delay. void SetTargetPlayoutDelay(base::TimeDelta playout_delay); - // Callback by |start_timeout_timer_|. - void OnOfferAnswerExchangeTimeout(); + media::VideoEncodeAccelerator::SupportedProfiles GetSupportedVeaProfiles(); + + // Create and send OFFER message. + void CreateAndSendOffer(); - SessionClient* client_ = nullptr; + // Provided by Cast Media Route Provider (MRP). + const int32_t session_id_; + const CastSinkInfo sink_info_; - // Create on StartInternal(). + SessionObserver* observer_ = nullptr; + ResourceProvider* resource_provider_ = nullptr; + MirrorSettings mirror_settings_; + + MessageDispatcher message_dispatcher_; + + network::mojom::NetworkContextPtr network_context_; + + base::Optional<SessionMonitor> session_monitor_; + + // Created after OFFER/ANSWER exchange succeeds. std::unique_ptr<AudioRtpStream> audio_stream_; std::unique_ptr<VideoRtpStream> video_stream_; std::unique_ptr<VideoCaptureClient> video_capture_client_; @@ -81,9 +119,6 @@ class Session final : public RtpStreamClient { scoped_refptr<base::SingleThreadTaskRunner> audio_encode_thread_ = nullptr; scoped_refptr<base::SingleThreadTaskRunner> video_encode_thread_ = nullptr; - // Fire if the OFFER/ANSWER exchange times out. - base::OneShotTimer start_timeout_timer_; - base::WeakPtrFactory<Session> weak_factory_; }; diff --git a/chromium/components/mirroring/service/session_monitor.cc b/chromium/components/mirroring/service/session_monitor.cc new file mode 100644 index 00000000000..94e6d77438a --- /dev/null +++ b/chromium/components/mirroring/service/session_monitor.cc @@ -0,0 +1,412 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/mirroring/service/session_monitor.h" + +#include <string> +#include <vector> + +#include "base/json/json_reader.h" +#include "base/json/json_writer.h" +#include "base/stl_util.h" +#include "components/mirroring/service/value_util.h" +#include "components/mirroring/service/wifi_status_monitor.h" +#include "components/version_info/version_info.h" +#include "media/cast/cast_environment.h" +#include "media/cast/logging/log_serializer.h" +#include "media/cast/logging/logging_defines.h" +#include "media/cast/logging/proto/raw_events.pb.h" +#include "media/cast/logging/raw_event_subscriber_bundle.h" +#include "net/traffic_annotation/network_traffic_annotation.h" +#include "services/network/public/cpp/resource_request.h" +#include "services/network/public/cpp/simple_url_loader.h" + +namespace mirroring { + +namespace { + +// Interval between snapshots of Cast Streaming events/stats. +constexpr base::TimeDelta kSnapshotInterval = + base::TimeDelta::FromMinutes(15); // Typical: 15 min → ~3 MB + +// The maximum number of bytes for receiver's setup info. 256kb should be more +// than sufficient. +constexpr int kMaxSetupResponseSizeBytes = 262144; + +// Returns the number of milliseconds elapsed since epoch. +int32_t ToEpochTime(const base::Time& time) { + return (time - base::Time::UnixEpoch()).InMilliseconds(); +} + +// Helper to parse the response for receiver setup info and update the tags. +bool AddReceiverSetupInfoTags(const std::string& response, base::Value* tags) { + DCHECK(tags); + std::unique_ptr<base::Value> value = base::JSONReader::Read(response); + + std::string build_version; + bool is_connected = false; + bool is_on_ethernet = false; + bool has_update = false; + int32_t uptime_seconds = 0; + + const bool result = + value && value->is_dict() && + GetString(*value, "cast_build_revision", &build_version) && + GetBool(*value, "connected", &is_connected) && + GetBool(*value, "ethernet_connected", &is_on_ethernet) && + GetBool(*value, "has_update", &has_update) && + GetInt(*value, "uptime", &uptime_seconds); + if (result) { + tags->SetKey("receiverVersion", base::Value(build_version)); + tags->SetKey("receiverConnected", base::Value(is_connected)); + tags->SetKey("receiverOnEthernet", base::Value(is_on_ethernet)); + tags->SetKey("receiverHasUpdatePending", base::Value(has_update)); + tags->SetKey("receiverUptimeSeconds", base::Value(uptime_seconds)); + } + return result; +} + +const char* ToErrorMessage(SessionError error) { + switch (error) { + case ANSWER_TIME_OUT: + return "ANSWER response time out"; + case ANSWER_NOT_OK: + return "Received an error ANSWER response"; + case ANSWER_MISMATCHED_CAST_MODE: + return "Unexpected cast mode in ANSWER response."; + case ANSWER_MISMATCHED_SSRC_LENGTH: + return "sendIndexes.length != ssrcs.length in ANSWER"; + case ANSWER_SELECT_MULTIPLE_AUDIO: + return "Receiver selected audio RTP stream twice in ANSWER"; + case ANSWER_SELECT_MULTIPLE_VIDEO: + return "Receiver selected video RTP stream twice in ANSWER"; + case ANSWER_SELECT_INVALID_INDEX: + return "Invalid indexes selected in ANSWER"; + case ANSWER_NO_AUDIO_OR_VIDEO: + return "Incorrect ANSWER message: No audio or Video."; + case AUDIO_CAPTURE_ERROR: + return "Audio capture error"; + case VIDEO_CAPTURE_ERROR: + return "Video capture error"; + case RTP_STREAM_ERROR: + return "RTP stream error"; + case ENCODING_ERROR: + return "Encoding status error"; + case CAST_TRANSPORT_ERROR: + return "Transport error"; + } + return ""; +} + +} // namespace + +SessionMonitor::SessionMonitor( + int max_retention_bytes, + const net::IPAddress& receiver_address, + base::Value session_tags, + network::mojom::URLLoaderFactoryPtr loader_factory, + std::unique_ptr<WifiStatusMonitor> wifi_status_monitor) + : max_retention_bytes_(max_retention_bytes), + receiver_address_(receiver_address), + session_tags_(std::move(session_tags)), + url_loader_factory_(std::move(loader_factory)), + wifi_status_monitor_(std::move(wifi_status_monitor)), + stored_snapshots_bytes_(0), + weak_factory_(this) {} + +SessionMonitor::~SessionMonitor() {} + +void SessionMonitor::StartStreamingSession( + scoped_refptr<media::cast::CastEnvironment> cast_environment, + SessionType session_type, + bool is_remoting) { + DCHECK(!event_subscribers_); + DCHECK(!snapshot_timer_.IsRunning()); + + std::string session_activity = + session_type == AUDIO_AND_VIDEO + ? "audio+video" + : session_type == AUDIO_ONLY ? "audio-only" : "video-only"; + session_activity += is_remoting ? " remoting" : " streaming"; + session_tags_.SetKey("activity", base::Value(session_activity)); + + // Query the receiver setup info at the beginning of each streaming session. + QueryReceiverSetupInfo(); + + // Start collecting Cast Streaming events/stats. + event_subscribers_ = std::make_unique<media::cast::RawEventSubscriberBundle>( + std::move(cast_environment)); + if (session_type != VIDEO_ONLY) + event_subscribers_->AddEventSubscribers(true /* is_audio */); + if (session_type != AUDIO_ONLY) + event_subscribers_->AddEventSubscribers(false /* is_audio */); + + // Start periodically snapshotting Cast Streaming events/stats. + snapshot_timer_.Start(FROM_HERE, kSnapshotInterval, + base::BindRepeating(&SessionMonitor::TakeSnapshot, + base::Unretained(this))); + + start_time_ = base::Time::Now(); +} + +void SessionMonitor::StopStreamingSession() { + if (snapshot_timer_.IsRunning()) { + snapshot_timer_.Stop(); + TakeSnapshot(); // Final snapshot of this streaming session. + } + event_subscribers_.reset(); +} + +void SessionMonitor::OnStreamingError(SessionError error) { + DVLOG(2) << error; + + if (!snapshot_timer_.IsRunning()) + return; // Ignore errors before streaming starts. + // If the error has already been recorded, do not overwrite it with another + // since the first will usually be the most indicative of the problem. + if (error_.has_value()) + return; + error_time_ = base::Time::Now(); + error_.emplace(error); +} + +std::vector<SessionMonitor::EventsAndStats> +SessionMonitor::AssembleBundlesAndClear( + const std::vector<int32_t>& bundle_sizes) { + std::vector<EventsAndStats> bundles; + // If a streaming session is currently active, take a snapshot now so that all + // data collected since the last automatic periodic snapshot is included in + // the bundle. + if (snapshot_timer_.IsRunning()) { + TakeSnapshot(); + snapshot_timer_.Reset(); + } + + for (int32_t max_bytes : bundle_sizes) + bundles.emplace_back(MakeSliceOfSnapshots(max_bytes)); + snapshots_.clear(); + stored_snapshots_bytes_ = 0; + return bundles; +} + +SessionMonitor::EventsAndStats SessionMonitor::MakeSliceOfSnapshots( + int32_t max_bytes) { + // Immediately subtract two bytes for array brackets ("[]") since + // AssembleSnapshotsAndClear() will produce a JSON array of each snapshot's + // stats JSON. + max_bytes -= 2; + base::circular_deque<EventsAndStats> slice; + for (int i = snapshots_.size() - 1; i >= 0; --i) { + max_bytes -= snapshots_[i].second.length() + 1 /* size of the comma */; + // If insufficient bytes remain to retain the current stats JSON, stop + // adding more Snapshots to the slice. + if (max_bytes < 0) + break; + slice.emplace_front(std::make_pair("", snapshots_[i].second)); + // If sufficient bytes remain to include the current events Blob, add it to + // the slice. + if (!snapshots_[i].first.empty()) { + const int32_t events_size = snapshots_[i].first.length(); + if (max_bytes >= events_size) { + slice[0].first = snapshots_[i].first; + max_bytes -= events_size; + } + } + } + + EventsAndStats bundle; + if (slice.empty()) + return bundle; + + bundle.second = "["; + for (size_t i = 0; i < slice.size(); i++) { + // To produce a single events gzipped-data Blob, simply concatenate the + // individual gzipped-data Blobs. The spec for gzip explicitly allows for + // this. :-) + bundle.first += slice[i].first; + // To produce the JSON stats array, concatenate the mix of string and Blob + // objects to produce a single UTF-8 encoded string. + if (i > 0) + bundle.second += ","; + bundle.second += slice[i].second; + } + bundle.second += "]"; + + return bundle; +} + +void SessionMonitor::TakeSnapshot() { + // Session-level tags. + base::Value tags = session_tags_.Clone(); + + // Add snapshot-level tags. + tags.SetKey("startTime", base::Value(ToEpochTime(start_time_))); + const base::Time end_time = base::Time::Now(); + tags.SetKey("endTime", base::Value(ToEpochTime(end_time))); + start_time_ = end_time; + + if (wifi_status_monitor_) { + const std::vector<WifiStatus> wifi_status = + wifi_status_monitor_->GetRecentValues(); + base::Value::ListStorage wifi_status_list; + for (const auto& status : wifi_status) { + base::Value status_value(base::Value::Type::DICTIONARY); + status_value.SetKey("wifiSnr", base::Value(status.snr)); + status_value.SetKey("wifiSpeed", base::Value(status.speed)); + status_value.SetKey("timestamp", + base::Value(ToEpochTime(status.timestamp))); + wifi_status_list.emplace_back(std::move(status_value)); + } + tags.SetKey("receiverWifiStatus", base::Value(wifi_status_list)); + } + + // Streaming error tags (if any). + if (error_.has_value()) { + tags.SetKey("streamingErrorTime", base::Value(ToEpochTime(error_time_))); + tags.SetKey("streamingErrorMessage", + base::Value(ToErrorMessage(error_.value()))); + error_.reset(); + } + + std::string tags_string; + base::JSONWriter::Write(tags, &tags_string); + + // Collect raw events. + std::string events = GetEventLogsAndReset(true, tags_string) + + GetEventLogsAndReset(false, tags_string); + + // Collect stats. + std::unique_ptr<base::DictionaryValue> audio_stats = + base::DictionaryValue::From(GetStatsAndReset(true)); + std::unique_ptr<base::DictionaryValue> video_stats = + base::DictionaryValue::From(GetStatsAndReset(false)); + base::DictionaryValue stats; + if (audio_stats) + stats.MergeDictionary(audio_stats.get()); + if (video_stats) + stats.MergeDictionary(video_stats.get()); + stats.SetKey("tags", std::move(tags)); + std::string stats_string; + base::JSONWriter::Write(stats, &stats_string); + + int snapshots_bytes = + stored_snapshots_bytes_ + events.size() + stats_string.size(); + // Prune |snapshots_| if necessary. + while (snapshots_bytes > max_retention_bytes_) { + snapshots_bytes -= snapshots_[0].first.size(); + snapshots_[0].first = std::string(); + if (snapshots_bytes <= max_retention_bytes_) + break; + snapshots_bytes -= snapshots_[0].second.size(); + snapshots_.pop_front(); + } + snapshots_.emplace_back(std::make_pair(events, stats_string)); + stored_snapshots_bytes_ = snapshots_bytes; +} + +std::string SessionMonitor::GetEventLogsAndReset( + bool is_audio, + const std::string& extra_data) { + std::string result; + if (!event_subscribers_.get()) + return result; + + media::cast::EncodingEventSubscriber* subscriber = + event_subscribers_->GetEncodingEventSubscriber(is_audio); + if (!subscriber) + return result; + + media::cast::proto::LogMetadata metadata; + media::cast::FrameEventList frame_events; + media::cast::PacketEventList packet_events; + + subscriber->GetEventsAndReset(&metadata, &frame_events, &packet_events); + + if (!extra_data.empty()) + metadata.set_extra_data(extra_data); + media::cast::proto::GeneralDescription* gen_desc = + metadata.mutable_general_description(); + gen_desc->set_product(version_info::GetProductName()); + gen_desc->set_product_version(version_info::GetVersionNumber()); + gen_desc->set_os(version_info::GetOSType()); + + result.resize(media::cast::kMaxSerializedBytes); + int output_bytes; + // TODO(xjz): media::cast::SerializeEvents() shouldn't require the caller to + // pre-allocate the memory. It should return a string result. + if (media::cast::SerializeEvents(metadata, frame_events, packet_events, + true /* compress */, + media::cast::kMaxSerializedBytes, + base::data(result), &output_bytes)) { + result.resize(output_bytes); + } else { + result.clear(); + } + return result; +} + +std::unique_ptr<base::Value> SessionMonitor::GetStatsAndReset(bool is_audio) { + if (!event_subscribers_.get()) + return nullptr; + + media::cast::StatsEventSubscriber* subscriber = + event_subscribers_->GetStatsEventSubscriber(is_audio); + if (!subscriber) + return nullptr; + + std::unique_ptr<base::Value> stats = subscriber->GetStats(); + subscriber->Reset(); + return stats; +} + +void SessionMonitor::QueryReceiverSetupInfo() { + auto resource_request = std::make_unique<network::ResourceRequest>(); + resource_request->method = "GET"; + resource_request->url = GURL("http://" + receiver_address_.ToString() + + ":8008/setup/eureka_info"); + net::NetworkTrafficAnnotationTag traffic_annotation = + net::DefineNetworkTrafficAnnotation("mirroring_get_setup_info", R"( + semantics { + sender: "Mirroring Service" + description: + "Mirroring Service sends a request to the receiver to obtain its " + "setup info such as the build version, the model name, etc. The " + "data is included in mirroring feedback for analysis." + trigger: + "A tab/desktop mirroring session starts." + data: "An HTTP GET request." + destination: OTHER + destination_other: + "A mirroring receiver, such as a ChromeCast device." + } + policy { + cookies_allowed: NO + setting: "This feature cannot be disabled in settings." + chrome_policy { + EnableMediaRouter { + EnableMediaRouter: false + } + } + })"); + std::unique_ptr<network::SimpleURLLoader> url_loader = + network::SimpleURLLoader::Create(std::move(resource_request), + traffic_annotation); + network::SimpleURLLoader* url_loader_ptr = url_loader.get(); + url_loader_ptr->DownloadToString( + url_loader_factory_.get(), + base::BindOnce( + [](base::WeakPtr<SessionMonitor> monitor, + std::unique_ptr<network::SimpleURLLoader> url_loader, + std::unique_ptr<std::string> response) { + if (monitor) { + if (url_loader->NetError() != net::OK || + !AddReceiverSetupInfoTags(*response, &monitor->session_tags_)) + VLOG(2) << "Unable to fetch/parse receiver setup info."; + } + }, + weak_factory_.GetWeakPtr(), std::move(url_loader)), + kMaxSetupResponseSizeBytes); +} + +} // namespace mirroring diff --git a/chromium/components/mirroring/service/session_monitor.h b/chromium/components/mirroring/service/session_monitor.h new file mode 100644 index 00000000000..ba4892345c2 --- /dev/null +++ b/chromium/components/mirroring/service/session_monitor.h @@ -0,0 +1,142 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_MIRRORING_SERVICE_SESSION_MONITOR_H_ +#define COMPONENTS_MIRRORING_SERVICE_SESSION_MONITOR_H_ + +#include "components/mirroring/service/interface.h" + +#include <memory> +#include <string> + +#include "base/containers/circular_deque.h" +#include "base/macros.h" +#include "base/memory/weak_ptr.h" +#include "base/optional.h" +#include "base/timer/timer.h" +#include "base/values.h" +#include "components/mirroring/service/interface.h" +#include "services/network/public/mojom/url_loader_factory.mojom.h" + +namespace media { +namespace cast { +class CastEnvironment; +class RawEventSubscriberBundle; +} // namespace cast +} // namespace media + +namespace mirroring { + +class WifiStatusMonitor; + +// Monitors a single mirroring session's multiple Cast Streaming "subsessions", +// collecting and managing the following information: +// +// 1. WiFi signal strength, SNR, etc. on the connection between sender and +// receiver. +// 2. Extra "tags": Information about both the sender and receiver, such as +// software versions, mirroring settings, and network configuration. +// 3. Snapshots of Cast Streaming event logs (frame- and packet-level events +// that will allow an analysis of the data flows and a diagnosing of any +// issues occurring in-the-wild). Start/StopStreamingSession() are called +// to notify this monitor whenever each (of multiple) Cast Streaming +// sessions starts and ends. +// +// Either during or at the end of a session, AssembleBundlesAndClear() is called +// to re-package all of the information across multiple snapshots together into +// one bundle, whose blobs can then be included in user feedback report uploads. +// +// To avoid unbounded memory use, older data is discarded automatically if too +// much is accumulating. +class SessionMonitor { + public: + using EventsAndStats = + std::pair<std::string /* events */, std::string /* stats */>; + SessionMonitor(int max_retention_bytes, + const net::IPAddress& receiver_address, + base::Value session_tags, + network::mojom::URLLoaderFactoryPtr loader_factory, + std::unique_ptr<WifiStatusMonitor> wifi_status_monitor); + + ~SessionMonitor(); + + enum SessionType { + AUDIO_ONLY, + VIDEO_ONLY, + AUDIO_AND_VIDEO, + }; + + // Notifies this monitor that it may now start/stop monitoring Cast Streaming + // events/stats. + void StartStreamingSession( + scoped_refptr<media::cast::CastEnvironment> cast_environment, + SessionType session_type, + bool is_remoting); + void StopStreamingSession(); + + // Called when error occurs. Only records the first error since last snapshot. + void OnStreamingError(SessionError error); + + // Assembles one or more bundles of data, for inclusion in user feedback + // reports. The snapshot history is cleared each time this method is called, + // and so no two calls to this method will return the same data. The caller + // may request that multiple bundles be produced. This is used, for example, + // to get one bundle that meets the upload size maximum in addition to + // another. The total data size in bytes is strictly less-than-or-equal to + // |bundle_sizes|. + std::vector<EventsAndStats> AssembleBundlesAndClear( + const std::vector<int32_t>& bundle_sizes); + + // Takes a snapshot of recent Cast Streaming events and statistics. + void TakeSnapshot(); + + private: + // Query the receiver for its current setup and uptime. + void QueryReceiverSetupInfo(); + + // Get Cast Streaming events/stats. + std::string GetEventLogsAndReset(bool is_audio, + const std::string& extra_data); + std::unique_ptr<base::Value> GetStatsAndReset(bool is_audio); + + // Assemble the most-recent events+stats snapshots to a bundle of a byte size + // less than or equal to |max_bytes|. + EventsAndStats MakeSliceOfSnapshots(int32_t max_bytes); + + const int max_retention_bytes_; // Maximum number of bytes to keep. + + const net::IPAddress receiver_address_; + + base::Value session_tags_; // Streaming session-level tags. + + network::mojom::URLLoaderFactoryPtr url_loader_factory_; + + // Monitors the WiFi status if not null. + const std::unique_ptr<WifiStatusMonitor> wifi_status_monitor_; + + std::unique_ptr<media::cast::RawEventSubscriberBundle> event_subscribers_; + + base::RepeatingTimer snapshot_timer_; + + // The time that the current snapshot starts. + base::Time start_time_; + + // The most recent snapshots, from oldest to newest. The total size of the + // data stored in this list is bounded by |max_retention_bytes_|. + base::circular_deque<EventsAndStats> snapshots_; + + // The number of bytes currently stored in |snapshots_|. + int stored_snapshots_bytes_; + + base::Time error_time_; + base::Optional<SessionError> error_; + + base::WeakPtrFactory<SessionMonitor> weak_factory_; + + DISALLOW_COPY_AND_ASSIGN(SessionMonitor); +}; + +} // namespace mirroring + +#endif // COMPONENTS_MIRRORING_SERVICE_SESSION_MONITOR_H_ diff --git a/chromium/components/mirroring/service/session_monitor_unittest.cc b/chromium/components/mirroring/service/session_monitor_unittest.cc new file mode 100644 index 00000000000..c887eaa0a9f --- /dev/null +++ b/chromium/components/mirroring/service/session_monitor_unittest.cc @@ -0,0 +1,397 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/mirroring/service/session_monitor.h" + +#include "base/bind.h" +#include "base/callback.h" +#include "base/json/json_reader.h" +#include "base/json/json_writer.h" +#include "base/macros.h" +#include "base/test/mock_callback.h" +#include "base/test/scoped_task_environment.h" +#include "base/time/default_tick_clock.h" +#include "components/mirroring/service/message_dispatcher.h" +#include "components/mirroring/service/mirror_settings.h" +#include "components/mirroring/service/value_util.h" +#include "components/mirroring/service/wifi_status_monitor.h" +#include "media/cast/cast_environment.h" +#include "media/cast/test/utility/net_utility.h" +#include "mojo/public/cpp/bindings/strong_binding.h" +#include "net/base/ip_endpoint.h" +#include "services/network/test/test_url_loader_factory.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace mirroring { + +namespace { + +constexpr int kRetentionBytes = 512 * 1024; // 512k. + +void VerifyStringValue(const base::Value& raw_value, + const std::string& key, + const std::string& expected_value) { + std::string data; + EXPECT_TRUE(GetString(raw_value, key, &data)); + EXPECT_EQ(expected_value, data); +} + +void VerifyBoolValue(const base::Value& raw_value, + const std::string& key, + bool expected_value) { + bool data; + EXPECT_TRUE(GetBool(raw_value, key, &data)); + EXPECT_EQ(expected_value, data); +} + +void VerifyIntValue(const base::Value& raw_value, + const std::string& key, + int32_t expected_value) { + int32_t data; + EXPECT_TRUE(GetInt(raw_value, key, &data)); + EXPECT_EQ(expected_value, data); +} + +void VerifyWifiStatus(const base::Value& raw_value, + double starting_snr, + int starting_speed, + int num_of_status) { + EXPECT_TRUE(raw_value.is_dict()); + auto* found = raw_value.FindKey("tags"); + EXPECT_TRUE(found && found->is_dict()); + auto* wifi_status = found->FindKey("receiverWifiStatus"); + EXPECT_TRUE(wifi_status && wifi_status->is_list()); + const base::Value::ListStorage& status_list = wifi_status->GetList(); + EXPECT_EQ(num_of_status, static_cast<int>(status_list.size())); + for (int i = 0; i < num_of_status; ++i) { + double snr = -1; + int32_t speed = -1; + int32_t timestamp = 0; + EXPECT_TRUE(GetDouble(status_list[i], "wifiSnr", &snr)); + EXPECT_EQ(starting_snr + i, snr); + EXPECT_TRUE(GetInt(status_list[i], "wifiSpeed", &speed)); + EXPECT_EQ(starting_speed + i, speed); + EXPECT_TRUE(GetInt(status_list[i], "timestamp", ×tamp)); + } +} + +} // namespace + +class SessionMonitorTest : public CastMessageChannel, public ::testing::Test { + public: + SessionMonitorTest() + : receiver_address_(media::cast::test::GetFreeLocalPort().address()), + message_dispatcher_(this, error_callback_.Get()) {} + ~SessionMonitorTest() override {} + + protected: + // CastMessageChannel implementation. + MOCK_METHOD1(Send, void(const CastMessage&)); + + void CreateSessionMonitor(int max_bytes, std::string* expected_settings) { + EXPECT_CALL(*this, Send(::testing::_)).Times(::testing::AtLeast(1)); + auto wifi_status_monitor = + std::make_unique<WifiStatusMonitor>(123, &message_dispatcher_); + network::mojom::URLLoaderFactoryPtr url_loader_factory; + auto test_url_loader_factory = + std::make_unique<network::TestURLLoaderFactory>(); + url_loader_factory_ = test_url_loader_factory.get(); + mojo::MakeStrongBinding(std::move(test_url_loader_factory), + mojo::MakeRequest(&url_loader_factory)); + MirrorSettings mirror_settings; + base::Value session_tags(base::Value::Type::DICTIONARY); + base::Value settings = mirror_settings.ToDictionaryValue(); + if (expected_settings) + EXPECT_TRUE(base::JSONWriter::Write(settings, expected_settings)); + session_tags.SetKey("mirrorSettings", std::move(settings)); + session_tags.SetKey("receiverProductName", base::Value("ChromeCast")); + session_tags.SetKey("shouldCaptureAudio", base::Value(true)); + session_tags.SetKey("shouldCaptureVideo", base::Value(true)); + session_monitor_ = std::make_unique<SessionMonitor>( + max_bytes, receiver_address_, std::move(session_tags), + std::move(url_loader_factory), std::move(wifi_status_monitor)); + } + + // Generates and sends |num_of_responses| WiFi status. + void SendWifiStatus(double starting_snr, + int starting_speed, + int num_of_responses) { + for (int i = 0; i < num_of_responses; ++i) { + CastMessage message; + message.message_namespace = kWebRtcNamespace; + message.json_format_data = + "{\"seqNum\":" + + std::to_string(message_dispatcher_.GetNextSeqNumber()) + + "," + "\"type\": \"STATUS_RESPONSE\"," + "\"result\": \"ok\"," + "\"status\": {" + "\"wifiSnr\":" + + std::to_string(starting_snr + i) + + "," + "\"wifiSpeed\": [1234, 5678, 3000, " + + std::to_string(starting_speed + i) + + "]," + "\"wifiFcsError\": [12, 13, 12, 12]}" // This will be ignored. + "}"; + static_cast<CastMessageChannel*>(&message_dispatcher_)->Send(message); + scoped_task_environment_.RunUntilIdle(); + } + } + + void StartStreamingSession() { + cast_environment_ = new media::cast::CastEnvironment( + base::DefaultTickClock::GetInstance(), + scoped_task_environment_.GetMainThreadTaskRunner(), + scoped_task_environment_.GetMainThreadTaskRunner(), + scoped_task_environment_.GetMainThreadTaskRunner()); + EXPECT_TRUE(session_monitor_); + session_monitor_->StartStreamingSession(cast_environment_, + SessionMonitor::AUDIO_AND_VIDEO, + false /* is_remoting */); + scoped_task_environment_.RunUntilIdle(); + } + + void StopStreamingSession() { + EXPECT_TRUE(session_monitor_); + session_monitor_->StopStreamingSession(); + cast_environment_ = nullptr; + scoped_task_environment_.RunUntilIdle(); + } + + std::vector<SessionMonitor::EventsAndStats> AssembleBundleAndVerify( + const std::vector<int32_t>& bundle_sizes) { + std::vector<SessionMonitor::EventsAndStats> bundles = + session_monitor_->AssembleBundlesAndClear(bundle_sizes); + scoped_task_environment_.RunUntilIdle(); + EXPECT_EQ(bundle_sizes.size(), bundles.size()); + for (size_t i = 0; i < bundles.size(); ++i) { + EXPECT_FALSE(bundles[i].first.empty()); + EXPECT_FALSE(bundles[i].second.empty()); + EXPECT_LE( + static_cast<int>(bundles[i].first.size() + bundles[i].second.size()), + bundle_sizes[i]); + } + return bundles; + } + + base::Value ReadStats(const std::string& stats_string) { + std::unique_ptr<base::Value> stats_ptr = + base::JSONReader::Read(stats_string); + EXPECT_TRUE(stats_ptr); + base::Value stats = base::Value::FromUniquePtrValue(std::move(stats_ptr)); + EXPECT_TRUE(stats.is_list()); + return stats; + } + + void SendReceiverSetupInfo(const std::string& setup_info) { + url_loader_factory_->AddResponse( + "http://" + receiver_address_.ToString() + ":8008/setup/eureka_info", + setup_info); + scoped_task_environment_.RunUntilIdle(); + } + + void TakeSnapshot() { + ASSERT_TRUE(session_monitor_); + session_monitor_->TakeSnapshot(); + scoped_task_environment_.RunUntilIdle(); + } + + void ReportError(SessionError error) { + ASSERT_TRUE(session_monitor_); + session_monitor_->OnStreamingError(error); + scoped_task_environment_.RunUntilIdle(); + } + + private: + base::test::ScopedTaskEnvironment scoped_task_environment_; + const net::IPAddress receiver_address_; + base::MockCallback<MessageDispatcher::ErrorCallback> error_callback_; + MessageDispatcher message_dispatcher_; + network::TestURLLoaderFactory* url_loader_factory_ = nullptr; + std::unique_ptr<SessionMonitor> session_monitor_; + scoped_refptr<media::cast::CastEnvironment> cast_environment_ = nullptr; + + DISALLOW_COPY_AND_ASSIGN(SessionMonitorTest); +}; + +TEST_F(SessionMonitorTest, ProvidesExpectedTags) { + std::string expected_settings; + CreateSessionMonitor(kRetentionBytes, &expected_settings); + SendWifiStatus(34, 2000, 5); + StartStreamingSession(); + std::vector<int32_t> bundle_sizes({kRetentionBytes}); + std::vector<SessionMonitor::EventsAndStats> bundles = + AssembleBundleAndVerify(bundle_sizes); + + base::Value stats = ReadStats(bundles[0].second); + const base::Value::ListStorage& stats_list = stats.GetList(); + ASSERT_EQ(1u, stats_list.size()); + // Verify tags. + EXPECT_TRUE(stats_list[0].is_dict()); + auto* found = stats_list[0].FindKey("video"); + EXPECT_TRUE(found && found->is_dict()); + found = stats_list[0].FindKey("audio"); + EXPECT_TRUE(found && found->is_dict()); + found = stats_list[0].FindKey("tags"); + EXPECT_TRUE(found && found->is_dict()); + // Verify session tags. + VerifyStringValue(*found, "activity", "audio+video streaming"); + VerifyStringValue(*found, "receiverProductName", "ChromeCast"); + VerifyBoolValue(*found, "shouldCaptureAudio", true); + VerifyBoolValue(*found, "shouldCaptureVideo", true); + auto* settings = found->FindKey("mirrorSettings"); + EXPECT_TRUE(settings && settings->is_dict()); + std::string settings_string; + EXPECT_TRUE(base::JSONWriter::Write(*settings, &settings_string)); + EXPECT_EQ(expected_settings, settings_string); + VerifyWifiStatus(stats_list[0], 34, 2000, 5); +} + +// Test for multiple streaming sessions. +TEST_F(SessionMonitorTest, MultipleSessions) { + CreateSessionMonitor(kRetentionBytes, nullptr); + StartStreamingSession(); + StopStreamingSession(); + // Starts the second streaming session. + StartStreamingSession(); + StopStreamingSession(); + std::vector<int32_t> bundle_sizes({kRetentionBytes}); + std::vector<SessionMonitor::EventsAndStats> bundles = + AssembleBundleAndVerify(bundle_sizes); + base::Value stats = ReadStats(bundles[0].second); + const base::Value::ListStorage& stats_list = stats.GetList(); + // There should be two sessions in the recorded stats. + EXPECT_EQ(2u, stats_list.size()); +} + +TEST_F(SessionMonitorTest, ConfigureMaxRetentionBytes) { + // 2500 is an estimate number of bytes for a snapshot that includes tags and + // five WiFi status records. + CreateSessionMonitor(2500, nullptr); + SendWifiStatus(34, 2000, 5); + StartStreamingSession(); + StopStreamingSession(); + SendWifiStatus(54, 3000, 5); + StartStreamingSession(); + StopStreamingSession(); + std::vector<int32_t> bundle_sizes({kRetentionBytes}); + std::vector<SessionMonitor::EventsAndStats> bundles = + AssembleBundleAndVerify(bundle_sizes); + base::Value stats = ReadStats(bundles[0].second); + const base::Value::ListStorage& stats_list = stats.GetList(); + // Expect to only record the second session. + ASSERT_EQ(1u, stats_list.size()); + VerifyWifiStatus(stats_list[0], 54, 3000, 5); +} + +TEST_F(SessionMonitorTest, AssembleBundlesWithVaryingSizes) { + CreateSessionMonitor(kRetentionBytes, nullptr); + SendWifiStatus(34, 2000, 5); + StartStreamingSession(); + StopStreamingSession(); + SendWifiStatus(54, 3000, 5); + StartStreamingSession(); + StopStreamingSession(); + std::vector<int32_t> bundle_sizes({2500, kRetentionBytes}); + std::vector<SessionMonitor::EventsAndStats> bundles = + AssembleBundleAndVerify(bundle_sizes); + + // Expect the first bundle has only one session. + base::Value stats = ReadStats(bundles[0].second); + const base::Value::ListStorage& stats_list = stats.GetList(); + // Expect to only record the second session. + ASSERT_EQ(1u, stats_list.size()); + VerifyWifiStatus(stats_list[0], 54, 3000, 5); + + // Expect the second bundle has both sessions. + stats = ReadStats(bundles[1].second); + const base::Value::ListStorage& stats_list2 = stats.GetList(); + ASSERT_EQ(2u, stats_list2.size()); + VerifyWifiStatus(stats_list2[0], 34, 2000, 5); + VerifyWifiStatus(stats_list2[1], 54, 3000, 5); +} + +TEST_F(SessionMonitorTest, ErrorTags) { + CreateSessionMonitor(kRetentionBytes, nullptr); + StartStreamingSession(); + TakeSnapshot(); // Take the first snapshot. + ReportError(VIDEO_CAPTURE_ERROR); + ReportError(RTP_STREAM_ERROR); + TakeSnapshot(); // Take the second snapshot. + StopStreamingSession(); // Take the third snapshot. + + std::vector<int32_t> bundle_sizes({kRetentionBytes}); + std::vector<SessionMonitor::EventsAndStats> bundles = + AssembleBundleAndVerify(bundle_sizes); + base::Value stats = ReadStats(bundles[0].second); + const base::Value::ListStorage& stats_list = stats.GetList(); + // There should be three snapshots in the bundle. + ASSERT_EQ(3u, stats_list.size()); + + // The first and the third snapshots should have no error tags. + auto* tags = stats_list[0].FindKey("tags"); + ASSERT_TRUE(tags); + EXPECT_FALSE(tags->FindKey("streamingErrorTime")); + EXPECT_FALSE(tags->FindKey("streamingErrorMessage")); + tags = stats_list[2].FindKey("tags"); + ASSERT_TRUE(tags); + EXPECT_FALSE(tags->FindKey("streamingErrorTime")); + EXPECT_FALSE(tags->FindKey("streamingErrorMessage")); + + // The second snapshot should have the error tags. Only the first error is + // recorded. + tags = stats_list[1].FindKey("tags"); + ASSERT_TRUE(tags && tags->FindKey("streamingErrorTime")); + VerifyStringValue(*tags, "streamingErrorMessage", "Video capture error"); +} + +TEST_F(SessionMonitorTest, ReceiverSetupInfo) { + CreateSessionMonitor(kRetentionBytes, nullptr); + StartStreamingSession(); + // This snapshot should have no receiver setup info tags. + TakeSnapshot(); + + const std::string receiver_setup_info = + "{" + "\"cast_build_revision\": \"1.26.0.1\"," + "\"connected\": true," + "\"ethernet_connected\": false," + "\"has_update\": false," + "\"uptime\": 132536 }"; + + SendReceiverSetupInfo(receiver_setup_info); + + // A final snapshot is taken and should have receiver setup info tags. + StopStreamingSession(); + + std::vector<int32_t> bundle_sizes({kRetentionBytes}); + std::vector<SessionMonitor::EventsAndStats> bundles = + AssembleBundleAndVerify(bundle_sizes); + base::Value stats = ReadStats(bundles[0].second); + const base::Value::ListStorage& stats_list = stats.GetList(); + // There should be two snapshots in the bundle. + EXPECT_EQ(2u, stats_list.size()); + + // The first snapshot should have no receiver setup info tags. + auto* tags = stats_list[0].FindKey("tags"); + ASSERT_TRUE(tags); + EXPECT_FALSE(tags->FindKey("receiverVersion")); + EXPECT_FALSE(tags->FindKey("receiverConnected")); + EXPECT_FALSE(tags->FindKey("receiverOnEthernet")); + EXPECT_FALSE(tags->FindKey("receiverHasUpdatePending")); + EXPECT_FALSE(tags->FindKey("receiverUptimeSeconds")); + + // The second snapshot should have the receiver setup info tags. + tags = stats_list[1].FindKey("tags"); + ASSERT_TRUE(tags); + VerifyStringValue(*tags, "receiverVersion", "1.26.0.1"); + VerifyBoolValue(*tags, "receiverConnected", true); + VerifyBoolValue(*tags, "receiverOnEthernet", false); + VerifyBoolValue(*tags, "receiverHasUpdatePending", false); + VerifyIntValue(*tags, "receiverUptimeSeconds", 132536); +} + +} // namespace mirroring diff --git a/chromium/components/mirroring/service/session_unittest.cc b/chromium/components/mirroring/service/session_unittest.cc index 751fa584225..973c0daed8a 100644 --- a/chromium/components/mirroring/service/session_unittest.cc +++ b/chromium/components/mirroring/service/session_unittest.cc @@ -6,17 +6,22 @@ #include "base/bind.h" #include "base/callback.h" +#include "base/json/json_reader.h" #include "base/macros.h" -#include "base/memory/weak_ptr.h" #include "base/run_loop.h" #include "base/test/scoped_task_environment.h" #include "base/test/simple_test_tick_clock.h" +#include "base/values.h" #include "components/mirroring/service/fake_network_service.h" #include "components/mirroring/service/fake_video_capture_host.h" #include "components/mirroring/service/interface.h" +#include "components/mirroring/service/mirror_settings.h" +#include "components/mirroring/service/receiver_response.h" +#include "components/mirroring/service/value_util.h" #include "media/cast/test/utility/default_config.h" #include "media/cast/test/utility/net_utility.h" #include "mojo/public/cpp/bindings/binding.h" +#include "net/base/ip_address.h" #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" @@ -27,74 +32,125 @@ using media::cast::Packet; namespace mirroring { -class SessionTest : public SessionClient, public ::testing::Test { +const int kSessionId = 5; + +class SessionTest : public ResourceProvider, + public SessionObserver, + public CastMessageChannel, + public ::testing::Test { public: - SessionTest() : weak_factory_(this) { + SessionTest() : receiver_endpoint_(media::cast::test::GetFreeLocalPort()) { testing_clock_.Advance(base::TimeTicks::Now() - base::TimeTicks()); } ~SessionTest() override { scoped_task_environment_.RunUntilIdle(); } - // SessionClient implemenation. + // SessionObserver implemenation. MOCK_METHOD1(OnError, void(SessionError)); MOCK_METHOD0(DidStart, void()); MOCK_METHOD0(DidStop, void()); - MOCK_METHOD0(OnOfferAnswerExchange, void()); + + // ResourceProvider implemenation. MOCK_METHOD0(OnGetVideoCaptureHost, void()); MOCK_METHOD0(OnGetNetworkContext, void()); + // Called when sends OFFER message. + MOCK_METHOD0(OnOffer, void()); + + // CastMessageHandler implementation. For outbound messages. + void Send(const CastMessage& message) { + EXPECT_TRUE(message.message_namespace == kWebRtcNamespace || + message.message_namespace == kRemotingNamespace); + std::unique_ptr<base::Value> value = + base::JSONReader::Read(message.json_format_data); + ASSERT_TRUE(value); + std::string message_type; + EXPECT_TRUE(GetString(*value, "type", &message_type)); + if (message_type == "OFFER") { + EXPECT_TRUE(GetInt(*value, "seqNum", &offer_sequence_number_)); + OnOffer(); + } + } + void GetVideoCaptureHost( media::mojom::VideoCaptureHostRequest request) override { video_host_ = std::make_unique<FakeVideoCaptureHost>(std::move(request)); OnGetVideoCaptureHost(); } - void GetNewWorkContext( + void GetNetworkContext( network::mojom::NetworkContextRequest request) override { network_context_ = std::make_unique<MockNetworkContext>(std::move(request)); OnGetNetworkContext(); } - void DoOfferAnswerExchange( - const std::vector<FrameSenderConfig>& audio_configs, - const std::vector<FrameSenderConfig>& video_configs, - GetAnswerCallback callback) override { - OnOfferAnswerExchange(); - std::move(callback).Run(FrameSenderConfig(), - media::cast::GetDefaultVideoSenderConfig()); + void SendAnswer() { + FrameSenderConfig config = MirrorSettings::GetDefaultVideoConfig( + media::cast::RtpPayloadType::VIDEO_VP8, + media::cast::Codec::CODEC_VIDEO_VP8); + std::vector<FrameSenderConfig> video_configs; + video_configs.emplace_back(config); + + auto answer = std::make_unique<Answer>(); + answer->udp_port = receiver_endpoint_.port(); + answer->send_indexes.push_back(0); + answer->ssrcs.push_back(32); + answer->cast_mode = "mirroring"; + + ReceiverResponse response; + response.result = "ok"; + response.type = ResponseType::ANSWER; + response.sequence_number = offer_sequence_number_; + response.answer = std::move(answer); + + session_->OnAnswer("mirroring", std::vector<FrameSenderConfig>(), + video_configs, response); } protected: + void CreateSession() { + CastSinkInfo sink_info; + sink_info.ip_address = receiver_endpoint_.address(); + sink_info.capability = DeviceCapability::AUDIO_AND_VIDEO; + // Expect to receive OFFER message when session is created. + base::RunLoop run_loop; + EXPECT_CALL(*this, OnGetNetworkContext()).Times(1); + EXPECT_CALL(*this, OnError(_)).Times(0); + EXPECT_CALL(*this, OnOffer()) + .WillOnce(InvokeWithoutArgs(&run_loop, &base::RunLoop::Quit)); + session_ = std::make_unique<Session>( + kSessionId, sink_info, gfx::Size(1920, 1080), this, this, this); + run_loop.Run(); + } + base::test::ScopedTaskEnvironment scoped_task_environment_; + const net::IPEndPoint receiver_endpoint_; base::SimpleTestTickClock testing_clock_; std::unique_ptr<Session> session_; std::unique_ptr<FakeVideoCaptureHost> video_host_; std::unique_ptr<MockNetworkContext> network_context_; - base::WeakPtrFactory<SessionTest> weak_factory_; + int32_t offer_sequence_number_ = -1; private: DISALLOW_COPY_AND_ASSIGN(SessionTest); }; TEST_F(SessionTest, Mirroring) { - // Start a mirroring session. + CreateSession(); + scoped_task_environment_.RunUntilIdle(); { + // Except mirroing session starts after receiving ANSWER message. base::RunLoop run_loop; EXPECT_CALL(*this, OnGetVideoCaptureHost()).Times(1); - EXPECT_CALL(*this, OnGetNetworkContext()).Times(1); - EXPECT_CALL(*this, OnOfferAnswerExchange()).Times(1); + EXPECT_CALL(*this, OnError(_)).Times(0); EXPECT_CALL(*this, DidStart()) .WillOnce(InvokeWithoutArgs(&run_loop, &base::RunLoop::Quit)); - session_ = - std::make_unique<Session>(SessionType::AUDIO_AND_VIDEO, - media::cast::test::GetFreeLocalPort(), this); + SendAnswer(); run_loop.Run(); } - scoped_task_environment_.RunUntilIdle(); - { base::RunLoop run_loop; // Expect to send out some UDP packets. @@ -106,7 +162,6 @@ TEST_F(SessionTest, Mirroring) { video_host_->SendOneFrame(gfx::Size(64, 32), testing_clock_.NowTicks()); run_loop.Run(); } - scoped_task_environment_.RunUntilIdle(); // Stop the session. @@ -121,4 +176,21 @@ TEST_F(SessionTest, Mirroring) { scoped_task_environment_.RunUntilIdle(); } +TEST_F(SessionTest, AnswerTimeout) { + CreateSession(); + scoped_task_environment_.RunUntilIdle(); + { + // Expect error. + base::RunLoop run_loop; + EXPECT_CALL(*this, OnGetVideoCaptureHost()).Times(0); + EXPECT_CALL(*this, DidStop()).Times(1); + EXPECT_CALL(*this, OnError(ANSWER_TIME_OUT)) + .WillOnce(InvokeWithoutArgs(&run_loop, &base::RunLoop::Quit)); + session_->OnAnswer("mirroring", std::vector<FrameSenderConfig>(), + std::vector<FrameSenderConfig>(), ReceiverResponse()); + run_loop.Run(); + } + scoped_task_environment_.RunUntilIdle(); +} + } // namespace mirroring diff --git a/chromium/components/mirroring/service/udp_socket_client.cc b/chromium/components/mirroring/service/udp_socket_client.cc index f7677692753..96923f88ee8 100644 --- a/chromium/components/mirroring/service/udp_socket_client.cc +++ b/chromium/components/mirroring/service/udp_socket_client.cc @@ -47,16 +47,18 @@ net::NetworkTrafficAnnotationTag GetNetworkTrafficAnnotationTag() { } // namespace UdpSocketClient::UdpSocketClient(const net::IPEndPoint& remote_endpoint, - network::mojom::NetworkContextPtr context, + network::mojom::NetworkContext* context, base::OnceClosure error_callback) : remote_endpoint_(remote_endpoint), - network_context_(std::move(context)), + network_context_(context), error_callback_(std::move(error_callback)), binding_(this), bytes_sent_(0), allow_sending_(false), num_packets_pending_receive_(0), - weak_factory_(this) {} + weak_factory_(this) { + DCHECK(network_context_); +} UdpSocketClient::~UdpSocketClient() {} diff --git a/chromium/components/mirroring/service/udp_socket_client.h b/chromium/components/mirroring/service/udp_socket_client.h index 2eda92f8d4d..c58a7ee3987 100644 --- a/chromium/components/mirroring/service/udp_socket_client.h +++ b/chromium/components/mirroring/service/udp_socket_client.h @@ -24,7 +24,7 @@ class UdpSocketClient final : public media::cast::PacketTransport, public network::mojom::UDPSocketReceiver { public: UdpSocketClient(const net::IPEndPoint& remote_endpoint, - network::mojom::NetworkContextPtr context, + network::mojom::NetworkContext* context, base::OnceClosure error_callback); ~UdpSocketClient() override; @@ -54,7 +54,7 @@ class UdpSocketClient final : public media::cast::PacketTransport, const base::Optional<net::IPEndPoint>& addr); const net::IPEndPoint remote_endpoint_; - const network::mojom::NetworkContextPtr network_context_; + network::mojom::NetworkContext* const network_context_; base::OnceClosure error_callback_; mojo::Binding<network::mojom::UDPSocketReceiver> binding_; diff --git a/chromium/components/mirroring/service/udp_socket_client_unittest.cc b/chromium/components/mirroring/service/udp_socket_client_unittest.cc index 9ec51e25aa2..b88f2c7adfd 100644 --- a/chromium/components/mirroring/service/udp_socket_client_unittest.cc +++ b/chromium/components/mirroring/service/udp_socket_client_unittest.cc @@ -30,11 +30,10 @@ namespace mirroring { class UdpSocketClientTest : public ::testing::Test { public: UdpSocketClientTest() { - network::mojom::NetworkContextPtr network_context_ptr; network_context_ = std::make_unique<MockNetworkContext>( - mojo::MakeRequest(&network_context_ptr)); + mojo::MakeRequest(&network_context_ptr_)); udp_transport_client_ = std::make_unique<UdpSocketClient>( - media::cast::test::GetFreeLocalPort(), std::move(network_context_ptr), + media::cast::test::GetFreeLocalPort(), network_context_ptr_.get(), base::OnceClosure()); } @@ -49,6 +48,7 @@ class UdpSocketClientTest : public ::testing::Test { protected: base::test::ScopedTaskEnvironment scoped_task_environment_; + network::mojom::NetworkContextPtr network_context_ptr_; std::unique_ptr<MockNetworkContext> network_context_; std::unique_ptr<UdpSocketClient> udp_transport_client_; std::unique_ptr<Packet> received_packet_; diff --git a/chromium/components/mirroring/service/value_util.cc b/chromium/components/mirroring/service/value_util.cc new file mode 100644 index 00000000000..63eec3d9999 --- /dev/null +++ b/chromium/components/mirroring/service/value_util.cc @@ -0,0 +1,95 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/mirroring/service/value_util.h" + +namespace mirroring { + +bool GetInt(const base::Value& value, const std::string& key, int32_t* result) { + auto* found = value.FindKey(key); + if (!found || found->is_none()) + return true; + if (found->is_int()) { + *result = found->GetInt(); + return true; + } + return false; +} + +bool GetDouble(const base::Value& value, + const std::string& key, + double* result) { + auto* found = value.FindKey(key); + if (!found || found->is_none()) + return true; + if (found->is_double()) { + *result = found->GetDouble(); + return true; + } + if (found->is_int()) { + *result = found->GetInt(); + return true; + } + return false; +} + +bool GetString(const base::Value& value, + const std::string& key, + std::string* result) { + auto* found = value.FindKey(key); + if (!found || found->is_none()) + return true; + if (found->is_string()) { + *result = found->GetString(); + return true; + } + return false; +} + +bool GetBool(const base::Value& value, const std::string& key, bool* result) { + auto* found = value.FindKey(key); + if (!found || found->is_none()) + return true; + if (found->is_bool()) { + *result = found->GetBool(); + return true; + } + return false; +} + +bool GetIntArray(const base::Value& value, + const std::string& key, + std::vector<int32_t>* result) { + auto* found = value.FindKey(key); + if (!found || found->is_none()) + return true; + if (!found->is_list()) + return false; + for (const auto& number_value : found->GetList()) { + if (number_value.is_int()) + result->emplace_back(number_value.GetInt()); + else + return false; + } + return true; +} + +bool GetStringArray(const base::Value& value, + const std::string& key, + std::vector<std::string>* result) { + auto* found = value.FindKey(key); + if (!found || found->is_none()) + return true; + if (!found->is_list()) + return false; + for (const auto& string_value : found->GetList()) { + if (string_value.is_string()) + result->emplace_back(string_value.GetString()); + else + return false; + } + return true; +} + +} // namespace mirroring diff --git a/chromium/components/mirroring/service/value_util.h b/chromium/components/mirroring/service/value_util.h new file mode 100644 index 00000000000..d3247747999 --- /dev/null +++ b/chromium/components/mirroring/service/value_util.h @@ -0,0 +1,39 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_MIRRORING_SERVICE_VALUE_UTIL_H_ +#define COMPONENTS_MIRRORING_SERVICE_VALUE_UTIL_H_ + +#include <string> + +#include "base/values.h" + +namespace mirroring { + +// Read certain type of data from dictionary |value| if |key| exits. Return +// false if |key| exists and the type of the data mismatches. Return true +// otherwise. + +bool GetInt(const base::Value& value, const std::string& key, int32_t* result); + +bool GetDouble(const base::Value& value, + const std::string& key, + double* result); + +bool GetString(const base::Value& value, + const std::string& key, + std::string* result); + +bool GetBool(const base::Value& value, const std::string& key, bool* result); + +bool GetIntArray(const base::Value& value, + const std::string& key, + std::vector<int32_t>* result); + +bool GetStringArray(const base::Value& value, + const std::string& key, + std::vector<std::string>* result); +} // namespace mirroring + +#endif // COMPONENTS_MIRRORING_SERVICE_VALUE_UTIL_H_ diff --git a/chromium/components/mirroring/service/video_capture_client.cc b/chromium/components/mirroring/service/video_capture_client.cc index bbbd088f87f..24ef5467d77 100644 --- a/chromium/components/mirroring/service/video_capture_client.cc +++ b/chromium/components/mirroring/service/video_capture_client.cc @@ -14,8 +14,10 @@ namespace { constexpr int kDeviceId = 0; } // namespace -VideoCaptureClient::VideoCaptureClient(media::mojom::VideoCaptureHostPtr host) - : video_capture_host_(std::move(host)), +VideoCaptureClient::VideoCaptureClient(const media::VideoCaptureParams& params, + media::mojom::VideoCaptureHostPtr host) + : params_(params), + video_capture_host_(std::move(host)), binding_(this), weak_factory_(this) { DCHECK(video_capture_host_); @@ -36,8 +38,7 @@ void VideoCaptureClient::Start(FrameDeliverCallback deliver_callback, media::mojom::VideoCaptureObserverPtr observer; binding_.Bind(mojo::MakeRequest(&observer)); - video_capture_host_->Start(kDeviceId, 0, media::VideoCaptureParams(), - std::move(observer)); + video_capture_host_->Start(kDeviceId, 0, params_, std::move(observer)); } void VideoCaptureClient::Stop() { @@ -63,7 +64,7 @@ void VideoCaptureClient::Resume(FrameDeliverCallback deliver_callback) { return; } frame_deliver_callback_ = std::move(deliver_callback); - video_capture_host_->Resume(kDeviceId, 0, media::VideoCaptureParams()); + video_capture_host_->Resume(kDeviceId, 0, params_); } void VideoCaptureClient::RequestRefreshFrame() { @@ -100,15 +101,15 @@ void VideoCaptureClient::OnStateChanged(media::mojom::VideoCaptureState state) { } } -void VideoCaptureClient::OnBufferCreated( +void VideoCaptureClient::OnNewBuffer( int32_t buffer_id, - mojo::ScopedSharedBufferHandle handle) { + media::mojom::VideoBufferHandlePtr buffer_handle) { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); - DCHECK(handle.is_valid()); DVLOG(3) << __func__ << ": buffer_id=" << buffer_id; + DCHECK(buffer_handle->is_shared_buffer_handle()); - const auto insert_result = - client_buffers_.emplace(std::make_pair(buffer_id, std::move(handle))); + const auto insert_result = client_buffers_.emplace(std::make_pair( + buffer_id, std::move(buffer_handle->get_shared_buffer_handle()))); DCHECK(insert_result.second); } @@ -118,13 +119,11 @@ void VideoCaptureClient::OnBufferReady(int32_t buffer_id, DVLOG(3) << __func__ << ": buffer_id=" << buffer_id; bool consume_buffer = !frame_deliver_callback_.is_null(); - if ((info->pixel_format != media::PIXEL_FORMAT_I420 && - info->pixel_format != media::PIXEL_FORMAT_Y16) || - info->storage_type != media::VideoPixelStorage::CPU) { + if (info->pixel_format != media::PIXEL_FORMAT_I420 && + info->pixel_format != media::PIXEL_FORMAT_Y16) { consume_buffer = false; - LOG(DFATAL) << "Wrong pixel format or storage, got pixel format:" - << VideoPixelFormatToString(info->pixel_format) - << ", storage:" << static_cast<int>(info->storage_type); + LOG(DFATAL) << "Wrong pixel format, got pixel format:" + << VideoPixelFormatToString(info->pixel_format); } if (!consume_buffer) { video_capture_host_->ReleaseBuffer(kDeviceId, buffer_id, -1.0); diff --git a/chromium/components/mirroring/service/video_capture_client.h b/chromium/components/mirroring/service/video_capture_client.h index 3c43cd0bb4e..a417dcdef2f 100644 --- a/chromium/components/mirroring/service/video_capture_client.h +++ b/chromium/components/mirroring/service/video_capture_client.h @@ -16,6 +16,7 @@ namespace media { class VideoFrame; +class VideoFrameMetadata; } // namespace media namespace mirroring { @@ -26,7 +27,8 @@ namespace mirroring { // received through the media::mojom::VideoCaptureObserver interface. class VideoCaptureClient : public media::mojom::VideoCaptureObserver { public: - explicit VideoCaptureClient(media::mojom::VideoCaptureHostPtr host); + VideoCaptureClient(const media::VideoCaptureParams& params, + media::mojom::VideoCaptureHostPtr host); ~VideoCaptureClient() override; using FrameDeliverCallback = base::RepeatingCallback<void( @@ -47,8 +49,8 @@ class VideoCaptureClient : public media::mojom::VideoCaptureObserver { // media::mojom::VideoCaptureObserver implementations. void OnStateChanged(media::mojom::VideoCaptureState state) override; - void OnBufferCreated(int32_t buffer_id, - mojo::ScopedSharedBufferHandle handle) override; + void OnNewBuffer(int32_t buffer_id, + media::mojom::VideoBufferHandlePtr buffer_handle) override; void OnBufferReady(int32_t buffer_id, media::mojom::VideoFrameInfoPtr info) override; void OnBufferDestroyed(int32_t buffer_id) override; @@ -64,6 +66,7 @@ class VideoCaptureClient : public media::mojom::VideoCaptureObserver { void OnClientBufferFinished(int buffer_id, double consumer_resource_utilization); + const media::VideoCaptureParams params_; const media::mojom::VideoCaptureHostPtr video_capture_host_; // Called when capturing failed to start. diff --git a/chromium/components/mirroring/service/video_capture_client_unittest.cc b/chromium/components/mirroring/service/video_capture_client_unittest.cc index a903d086508..0e8637d84b7 100644 --- a/chromium/components/mirroring/service/video_capture_client_unittest.cc +++ b/chromium/components/mirroring/service/video_capture_client_unittest.cc @@ -8,6 +8,8 @@ #include "base/test/mock_callback.h" #include "base/test/scoped_task_environment.h" #include "components/mirroring/service/fake_video_capture_host.h" +#include "media/base/video_frame.h" +#include "media/base/video_frame_metadata.h" #include "mojo/public/cpp/bindings/binding.h" #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" @@ -26,8 +28,7 @@ media::mojom::VideoFrameInfoPtr GetVideoFrameInfo(const gfx::Size& size) { base::TimeTicks()); return media::mojom::VideoFrameInfo::New( base::TimeDelta(), metadata.GetInternalValues().Clone(), - media::PIXEL_FORMAT_I420, media::VideoPixelStorage::CPU, size, - gfx::Rect(size)); + media::PIXEL_FORMAT_I420, size, gfx::Rect(size)); } } // namespace @@ -38,7 +39,8 @@ class VideoCaptureClientTest : public ::testing::Test { media::mojom::VideoCaptureHostPtr host; host_impl_ = std::make_unique<FakeVideoCaptureHost>(mojo::MakeRequest(&host)); - client_ = std::make_unique<VideoCaptureClient>(std::move(host)); + client_ = std::make_unique<VideoCaptureClient>(media::VideoCaptureParams(), + std::move(host)); } ~VideoCaptureClientTest() override { @@ -79,7 +81,12 @@ TEST_F(VideoCaptureClientTest, Basic) { run_loop.Run(); } scoped_task_environment_.RunUntilIdle(); - client_->OnBufferCreated(0, mojo::SharedBufferHandle::Create(100000)); + + media::mojom::VideoBufferHandlePtr buffer_handle = + media::mojom::VideoBufferHandle::New(); + buffer_handle->set_shared_buffer_handle( + mojo::SharedBufferHandle::Create(100000)); + client_->OnNewBuffer(0, std::move(buffer_handle)); scoped_task_environment_.RunUntilIdle(); { base::RunLoop run_loop; diff --git a/chromium/components/mirroring/service/wifi_status_monitor.cc b/chromium/components/mirroring/service/wifi_status_monitor.cc new file mode 100644 index 00000000000..fa676b4a87d --- /dev/null +++ b/chromium/components/mirroring/service/wifi_status_monitor.cc @@ -0,0 +1,80 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/mirroring/service/wifi_status_monitor.h" + +#include "base/bind.h" +#include "base/callback.h" +#include "base/json/json_writer.h" +#include "base/logging.h" +#include "components/mirroring/service/message_dispatcher.h" + +namespace mirroring { + +namespace { + +// The interval to query the status. +constexpr base::TimeDelta kQueryInterval = base::TimeDelta::FromMinutes(2); + +// The maximum number of recent status to be kept. +constexpr int kMaxRecords = 30; + +} // namespace + +WifiStatusMonitor::WifiStatusMonitor(int32_t session_id, + MessageDispatcher* message_dispatcher) + : session_id_(session_id), message_dispatcher_(message_dispatcher) { + DCHECK(message_dispatcher_); + message_dispatcher_->Subscribe( + ResponseType::STATUS_RESPONSE, + base::BindRepeating(&WifiStatusMonitor::RecordStatus, + base::Unretained(this))); + query_timer_.Start(FROM_HERE, kQueryInterval, + base::BindRepeating(&WifiStatusMonitor::QueryStatus, + base::Unretained(this))); + QueryStatus(); +} + +WifiStatusMonitor::~WifiStatusMonitor() { + message_dispatcher_->Unsubscribe(ResponseType::STATUS_RESPONSE); +} + +std::vector<WifiStatus> WifiStatusMonitor::GetRecentValues() { + std::vector<WifiStatus> recent_status(recent_status_.begin(), + recent_status_.end()); + recent_status_.clear(); + return recent_status; +} + +void WifiStatusMonitor::QueryStatus() { + base::Value query(base::Value::Type::DICTIONARY); + query.SetKey("type", base::Value("GET_STATUS")); + query.SetKey("sessionId", base::Value(session_id_)); + query.SetKey("seqNum", base::Value(message_dispatcher_->GetNextSeqNumber())); + base::Value::ListStorage status; + status.emplace_back(base::Value("wifiSnr")); + status.emplace_back(base::Value("wifiSpeed")); + query.SetKey("get_status", base::Value(status)); + CastMessage get_status_message; + get_status_message.message_namespace = kWebRtcNamespace; + const bool did_serialize_query = + base::JSONWriter::Write(query, &get_status_message.json_format_data); + DCHECK(did_serialize_query); + message_dispatcher_->SendOutboundMessage(get_status_message); +} + +void WifiStatusMonitor::RecordStatus(const ReceiverResponse& response) { + if (!response.status || response.status->wifi_speed.size() != 4) + return; + if (recent_status_.size() == kMaxRecords) + recent_status_.pop_front(); + WifiStatus received_status; + received_status.snr = response.status->wifi_snr; + // Only records the current speed. + received_status.speed = response.status->wifi_speed[3]; + received_status.timestamp = base::Time::Now(); + recent_status_.emplace_back(received_status); +} + +} // namespace mirroring diff --git a/chromium/components/mirroring/service/wifi_status_monitor.h b/chromium/components/mirroring/service/wifi_status_monitor.h new file mode 100644 index 00000000000..ac348ce6d3e --- /dev/null +++ b/chromium/components/mirroring/service/wifi_status_monitor.h @@ -0,0 +1,61 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_MIRRORING_SERVICE_WIFI_STATUS_MONITOR_H_ +#define COMPONENTS_MIRRORING_SERVICE_WIFI_STATUS_MONITOR_H_ + +#include <vector> + +#include "base/containers/circular_deque.h" +#include "base/macros.h" +#include "base/time/time.h" +#include "base/timer/timer.h" + +namespace mirroring { + +class MessageDispatcher; +struct ReceiverResponse; + +struct WifiStatus { + double snr; + int32_t speed; // The current WiFi speed. + base::Time timestamp; // Recording time of this status. +}; + +// Periodically sends requests to the Cast device for WiFi network status +// updates, processes responses, and maintains a recent history of data points. +// This data can be included in feedback logs to help identify and diagnose +// issues related to lousy network performance. +class WifiStatusMonitor { + public: + // |message_dispatcher| must keep alive during the lifetime of this class. + WifiStatusMonitor(int32_t session_id, MessageDispatcher* message_dispatcher); + ~WifiStatusMonitor(); + + // Gets the recorded status and clear |recent_status_|. + std::vector<WifiStatus> GetRecentValues(); + + // Sends GET_STATUS message to receiver. + void QueryStatus(); + + // Callback for the STATUS_RESPONSE message. Records the WiFi status reported + // by receiver. + void RecordStatus(const ReceiverResponse& response); + + private: + const int32_t session_id_; + + MessageDispatcher* const message_dispatcher_; // Outlives this class. + + base::RepeatingTimer query_timer_; + + // Stores the recent status. Will be reset when GetRecentValues() is called. + base::circular_deque<WifiStatus> recent_status_; + + DISALLOW_COPY_AND_ASSIGN(WifiStatusMonitor); +}; + +} // namespace mirroring + +#endif // COMPONENTS_MIRRORING_SERVICE_WIFI_STATUS_MONITOR_H_ diff --git a/chromium/components/mirroring/service/wifi_status_monitor_unittest.cc b/chromium/components/mirroring/service/wifi_status_monitor_unittest.cc new file mode 100644 index 00000000000..0f7aac1a072 --- /dev/null +++ b/chromium/components/mirroring/service/wifi_status_monitor_unittest.cc @@ -0,0 +1,173 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/mirroring/service/wifi_status_monitor.h" + +#include "base/bind.h" +#include "base/callback.h" +#include "base/json/json_reader.h" +#include "base/macros.h" +#include "base/test/mock_callback.h" +#include "base/test/scoped_task_environment.h" +#include "base/values.h" +#include "components/mirroring/service/message_dispatcher.h" +#include "components/mirroring/service/value_util.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using ::testing::_; + +namespace mirroring { + +namespace { + +bool IsNullMessage(const CastMessage& message) { + return message.message_namespace.empty() && message.json_format_data.empty(); +} + +std::string GetMessageType(const CastMessage& message) { + std::string type; + std::unique_ptr<base::Value> value = + base::JSONReader::Read(message.json_format_data); + EXPECT_TRUE(value); + EXPECT_TRUE(GetString(*value, "type", &type)); + return type; +} + +void VerifyRecordedStatus(const std::vector<WifiStatus> recorded_status, + double starting_snr, + int starting_speed, + int num_of_responses) { + EXPECT_EQ(num_of_responses, static_cast<int>(recorded_status.size())); + for (int i = 0; i < num_of_responses; ++i) { + EXPECT_EQ(starting_snr + i, recorded_status[i].snr); + EXPECT_EQ(starting_speed + i, recorded_status[i].speed); + } +} + +} // namespace + +class WifiStatusMonitorTest : public CastMessageChannel, + public ::testing::Test { + public: + WifiStatusMonitorTest() : message_dispatcher_(this, error_callback_.Get()) {} + + ~WifiStatusMonitorTest() override {} + + // CastMessageChannel implementation. For outbound messages. + void Send(const CastMessage& message) override { + last_outbound_message_.message_namespace = message.message_namespace; + last_outbound_message_.json_format_data = message.json_format_data; + } + + protected: + // Generates and sends |num_of_responses| responses. + void SendStatusResponses(double starting_snr, + int starting_speed, + int num_of_responses) { + for (int i = 0; i < num_of_responses; ++i) { + const std::string response = + "{\"seqNum\":" + + std::to_string(message_dispatcher_.GetNextSeqNumber()) + + "," + "\"type\": \"STATUS_RESPONSE\"," + "\"result\": \"ok\"," + "\"status\": {" + "\"wifiSnr\":" + + std::to_string(starting_snr + i) + + "," + "\"wifiSpeed\": [1234, 5678, 3000, " + + std::to_string(starting_speed + i) + + "]," + "\"wifiFcsError\": [12, 13, 12, 12]}" // This will be ignored. + "}"; + SendInboundMessage(response); + } + } + + // Sends an inbound message to |message_dispatcher|. + void SendInboundMessage(const std::string& response) { + CastMessage message; + message.message_namespace = kWebRtcNamespace; + message.json_format_data = response; + static_cast<CastMessageChannel*>(&message_dispatcher_)->Send(message); + scoped_task_environment_.RunUntilIdle(); + } + + // Creates a WifiStatusMonitor and start monitoring the status. + std::unique_ptr<WifiStatusMonitor> StartMonitoring() { + EXPECT_TRUE(IsNullMessage(last_outbound_message_)); + EXPECT_CALL(error_callback_, Run(_)).Times(0); + auto status_monitor = + std::make_unique<WifiStatusMonitor>(123, &message_dispatcher_); + scoped_task_environment_.RunUntilIdle(); + // Expect to receive request to send GET_STATUS message when create a + // WifiStatusMonitor. + EXPECT_EQ(kWebRtcNamespace, last_outbound_message_.message_namespace); + EXPECT_EQ("GET_STATUS", GetMessageType(last_outbound_message_)); + // Clear the old outbound message. + last_outbound_message_.message_namespace.clear(); + last_outbound_message_.json_format_data.clear(); + EXPECT_TRUE(IsNullMessage(last_outbound_message_)); + return status_monitor; + } + + base::test::ScopedTaskEnvironment scoped_task_environment_; + base::MockCallback<MessageDispatcher::ErrorCallback> error_callback_; + MessageDispatcher message_dispatcher_; + CastMessage last_outbound_message_; + + private: + DISALLOW_COPY_AND_ASSIGN(WifiStatusMonitorTest); +}; + +TEST_F(WifiStatusMonitorTest, QueryStatusAndRecordResponse) { + std::unique_ptr<WifiStatusMonitor> status_monitor = StartMonitoring(); + + // Send two responses and verify the data are stored. + SendStatusResponses(36.7, 3001, 2); + std::vector<WifiStatus> recent_status = status_monitor->GetRecentValues(); + VerifyRecordedStatus(recent_status, 36.7, 3001, 2); + + // There should be no further status stored. + recent_status = status_monitor->GetRecentValues(); + EXPECT_TRUE(recent_status.empty()); + + // Sends more than the maximum number (30) of records that can be stored. + SendStatusResponses(36.7, 3001, 40); + // Expect that only the recent 30 records were stored. + recent_status = status_monitor->GetRecentValues(); + VerifyRecordedStatus(recent_status, 46.7, 3011, 30); +} + +TEST_F(WifiStatusMonitorTest, IgnoreMalformedStatusMessage) { + std::unique_ptr<WifiStatusMonitor> status_monitor = StartMonitoring(); + + // Sends a response with incomplete wifiSpeed data and expects it is ignored. + const std::string response1 = + "{\"seqNum\": 123," + "\"type\": \"STATUS_RESPONSE\"," + "\"result\": \"ok\"," + "\"status\": {" + "\"wifiSnr\": 32," + "\"wifiSpeed\": [1234, 5678, 3000]," + "\"wifiFcsError\": [12, 13, 12, 12]}" + "}"; + SendInboundMessage(response1); + std::vector<WifiStatus> recent_status = status_monitor->GetRecentValues(); + scoped_task_environment_.RunUntilIdle(); + EXPECT_TRUE(recent_status.empty()); + + // Sends a response with null status field and expects it is ignored. + const std::string response2 = + "{\"seqNum\": 123," + "\"type\": \"STATUS_RESPONSE\"," + "\"status\": null}"; + SendInboundMessage(response2); + recent_status = status_monitor->GetRecentValues(); + scoped_task_environment_.RunUntilIdle(); + EXPECT_TRUE(recent_status.empty()); +} + +} // namespace mirroring |