| // Copyright (c) 2020 The Chromium Embedded Framework 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 "tests/cefclient/browser/media_router_test.h" |
| |
| #include <string> |
| #include <vector> |
| |
| #include "include/base/cef_logging.h" |
| #include "include/cef_media_router.h" |
| #include "include/cef_parser.h" |
| #include "tests/cefclient/browser/test_runner.h" |
| |
| namespace client { |
| namespace media_router_test { |
| |
| namespace { |
| |
| const char kTestUrlPath[] = "/media_router"; |
| |
| // Application-specific error codes. |
| const int kMessageFormatError = 1; |
| const int kRequestFailedError = 2; |
| |
| // Message strings. |
| const char kNameKey[] = "name"; |
| const char kNameValueSubscribe[] = "subscribe"; |
| const char kNameValueCreateRoute[] = "createRoute"; |
| const char kNameValueTerminateRoute[] = "terminateRoute"; |
| const char kNameValueSendMessage[] = "sendMessage"; |
| const char kSourceKey[] = "source_urn"; |
| const char kSinkKey[] = "sink_id"; |
| const char kRouteKey[] = "route_id"; |
| const char kMessageKey[] = "message"; |
| const char kSuccessKey[] = "success"; |
| const char kPayloadKey[] = "payload"; |
| |
| // Convert a dictionary value to a JSON string. |
| CefString GetJSON(CefRefPtr<CefDictionaryValue> dictionary) { |
| CefRefPtr<CefValue> value = CefValue::Create(); |
| value->SetDictionary(dictionary); |
| return CefWriteJSON(value, JSON_WRITER_DEFAULT); |
| } |
| |
| typedef CefMessageRouterBrowserSide::Callback CallbackType; |
| |
| void SendSuccess(CefRefPtr<CallbackType> callback, |
| CefRefPtr<CefDictionaryValue> result) { |
| callback->Success(GetJSON(result)); |
| } |
| |
| void SendFailure(CefRefPtr<CallbackType> callback, |
| int error_code, |
| const std::string& error_message) { |
| callback->Failure(error_code, error_message); |
| } |
| |
| // Callback for CefMediaRouter::CreateRoute. |
| class MediaRouteCreateCallback : public CefMediaRouteCreateCallback { |
| public: |
| explicit MediaRouteCreateCallback(CefRefPtr<CallbackType> create_callback) |
| : create_callback_(create_callback) {} |
| |
| // CefMediaRouteCreateCallback method: |
| void OnMediaRouteCreateFinished(RouteCreateResult result, |
| const CefString& error, |
| CefRefPtr<CefMediaRoute> route) OVERRIDE { |
| CEF_REQUIRE_UI_THREAD(); |
| if (result == CEF_MRCR_OK) { |
| CefRefPtr<CefDictionaryValue> dict = CefDictionaryValue::Create(); |
| dict->SetString(kRouteKey, route->GetId()); |
| SendSuccess(create_callback_, dict); |
| } else { |
| SendFailure(create_callback_, kRequestFailedError + result, error); |
| } |
| create_callback_ = NULL; |
| } |
| |
| private: |
| CefRefPtr<CallbackType> create_callback_; |
| |
| IMPLEMENT_REFCOUNTING(MediaRouteCreateCallback); |
| DISALLOW_COPY_AND_ASSIGN(MediaRouteCreateCallback); |
| }; |
| |
| // Observes MediaRouter events. Only accessed on the UI thread. |
| class MediaObserver : public CefMediaObserver { |
| public: |
| typedef std::vector<CefRefPtr<CefMediaRoute>> MediaRouteVector; |
| typedef std::vector<CefRefPtr<CefMediaSink>> MediaSinkVector; |
| |
| MediaObserver(CefRefPtr<CefMediaRouter> media_router, |
| CefRefPtr<CallbackType> subscription_callback) |
| : media_router_(media_router), |
| subscription_callback_(subscription_callback), |
| next_sink_query_id_(0), |
| pending_sink_query_id_(-1), |
| pending_sink_callbacks_(0U) {} |
| |
| ~MediaObserver() OVERRIDE { ClearSinkInfoMap(); } |
| |
| bool CreateRoute(const std::string& source_urn, |
| const std::string& sink_id, |
| CefRefPtr<CallbackType> callback, |
| std::string& error) { |
| CefRefPtr<CefMediaSource> source = GetSource(source_urn); |
| if (!source) { |
| error = "Invalid source: " + source_urn; |
| return false; |
| } |
| |
| CefRefPtr<CefMediaSink> sink = GetSink(sink_id); |
| if (!sink) { |
| error = "Invalid sink: " + sink_id; |
| return false; |
| } |
| |
| media_router_->CreateRoute(source, sink, |
| new MediaRouteCreateCallback(callback)); |
| return true; |
| } |
| |
| bool TerminateRoute(const std::string& route_id, std::string& error) { |
| CefRefPtr<CefMediaRoute> route = GetRoute(route_id); |
| if (!route) { |
| error = "Invalid route: " + route_id; |
| return false; |
| } |
| |
| route->Terminate(); |
| return true; |
| } |
| |
| bool SendRouteMessage(const std::string& route_id, |
| const std::string& message, |
| std::string& error) { |
| CefRefPtr<CefMediaRoute> route = GetRoute(route_id); |
| if (!route) { |
| error = "Invalid route: " + route_id; |
| return false; |
| } |
| |
| route->SendRouteMessage(message.c_str(), message.size()); |
| return true; |
| } |
| |
| protected: |
| class DeviceInfoCallback : public CefMediaSinkDeviceInfoCallback { |
| public: |
| // Callback to be executed when the device info is available. |
| typedef base::Callback<void(const std::string& sink_id, |
| const CefMediaSinkDeviceInfo& device_info)> |
| CallbackType; |
| |
| DeviceInfoCallback(const std::string& sink_id, const CallbackType& callback) |
| : sink_id_(sink_id), callback_(callback) {} |
| |
| void OnMediaSinkDeviceInfo( |
| const CefMediaSinkDeviceInfo& device_info) OVERRIDE { |
| CEF_REQUIRE_UI_THREAD(); |
| callback_.Run(sink_id_, device_info); |
| callback_.Reset(); |
| } |
| |
| private: |
| const std::string sink_id_; |
| CallbackType callback_; |
| |
| IMPLEMENT_REFCOUNTING(DeviceInfoCallback); |
| DISALLOW_COPY_AND_ASSIGN(DeviceInfoCallback); |
| }; |
| |
| // CefMediaObserver methods: |
| void OnSinks(const MediaSinkVector& sinks) OVERRIDE { |
| CEF_REQUIRE_UI_THREAD(); |
| |
| ClearSinkInfoMap(); |
| |
| // Reset pending sink state. |
| pending_sink_callbacks_ = sinks.size(); |
| pending_sink_query_id_ = ++next_sink_query_id_; |
| |
| if (sinks.empty()) { |
| // No sinks, send the response immediately. |
| SendSinksResponse(); |
| return; |
| } |
| |
| DeviceInfoCallback::CallbackType callback = base::Bind( |
| &MediaObserver::OnSinkDeviceInfo, this, pending_sink_query_id_); |
| |
| MediaSinkVector::const_iterator it = sinks.begin(); |
| for (size_t idx = 0; it != sinks.end(); ++it, ++idx) { |
| CefRefPtr<CefMediaSink> sink = *it; |
| const std::string& sink_id = sink->GetId(); |
| SinkInfo* info = new SinkInfo; |
| info->sink = sink; |
| sink_info_map_.insert(std::make_pair(sink_id, info)); |
| |
| // Request the device info asynchronously. Send the response once all |
| // callbacks have executed. |
| sink->GetDeviceInfo(new DeviceInfoCallback(sink_id, callback)); |
| } |
| } |
| |
| void OnRoutes(const MediaRouteVector& routes) OVERRIDE { |
| CEF_REQUIRE_UI_THREAD(); |
| |
| route_map_.clear(); |
| |
| CefRefPtr<CefDictionaryValue> payload = CefDictionaryValue::Create(); |
| CefRefPtr<CefListValue> routes_list = CefListValue::Create(); |
| routes_list->SetSize(routes.size()); |
| |
| MediaRouteVector::const_iterator it = routes.begin(); |
| for (size_t idx = 0; it != routes.end(); ++it, ++idx) { |
| CefRefPtr<CefMediaRoute> route = *it; |
| const std::string& route_id = route->GetId(); |
| route_map_.insert(std::make_pair(route_id, route)); |
| |
| CefRefPtr<CefDictionaryValue> route_dict = CefDictionaryValue::Create(); |
| route_dict->SetString("id", route_id); |
| route_dict->SetString(kSourceKey, route->GetSource()->GetId()); |
| route_dict->SetString(kSinkKey, route->GetSink()->GetId()); |
| routes_list->SetDictionary(idx, route_dict); |
| } |
| |
| payload->SetList("routes_list", routes_list); |
| SendResponse("onRoutes", payload); |
| } |
| |
| void OnRouteStateChanged(CefRefPtr<CefMediaRoute> route, |
| ConnectionState state) OVERRIDE { |
| CEF_REQUIRE_UI_THREAD(); |
| |
| CefRefPtr<CefDictionaryValue> payload = CefDictionaryValue::Create(); |
| payload->SetString(kRouteKey, route->GetId()); |
| payload->SetInt("connection_state", state); |
| SendResponse("onRouteStateChanged", payload); |
| } |
| |
| void OnRouteMessageReceived(CefRefPtr<CefMediaRoute> route, |
| const void* message, |
| size_t message_size) OVERRIDE { |
| CEF_REQUIRE_UI_THREAD(); |
| |
| std::string message_str(static_cast<const char*>(message), message_size); |
| |
| CefRefPtr<CefDictionaryValue> payload = CefDictionaryValue::Create(); |
| payload->SetString(kRouteKey, route->GetId()); |
| payload->SetString(kMessageKey, message_str); |
| SendResponse("onRouteMessageReceived", payload); |
| } |
| |
| private: |
| CefRefPtr<CefMediaSource> GetSource(const std::string& source_urn) { |
| CefRefPtr<CefMediaSource> source = media_router_->GetSource(source_urn); |
| if (!source || !source->IsValid()) |
| return NULL; |
| return source; |
| } |
| |
| CefRefPtr<CefMediaSink> GetSink(const std::string& sink_id) { |
| SinkInfoMap::const_iterator it = sink_info_map_.find(sink_id); |
| if (it != sink_info_map_.end()) |
| return it->second->sink; |
| return NULL; |
| } |
| |
| void ClearSinkInfoMap() { |
| SinkInfoMap::const_iterator it = sink_info_map_.begin(); |
| for (; it != sink_info_map_.end(); ++it) { |
| delete it->second; |
| } |
| sink_info_map_.clear(); |
| } |
| |
| void OnSinkDeviceInfo(int sink_query_id, |
| const std::string& sink_id, |
| const CefMediaSinkDeviceInfo& device_info) { |
| // Discard callbacks that arrive after a new call to OnSinks(). |
| if (sink_query_id != pending_sink_query_id_) |
| return; |
| |
| SinkInfoMap::const_iterator it = sink_info_map_.find(sink_id); |
| if (it != sink_info_map_.end()) { |
| it->second->device_info = device_info; |
| } |
| |
| // Send the response once we've received all expected callbacks. |
| DCHECK_GT(pending_sink_callbacks_, 0U); |
| if (--pending_sink_callbacks_ == 0U) { |
| SendSinksResponse(); |
| } |
| } |
| |
| CefRefPtr<CefMediaRoute> GetRoute(const std::string& route_id) { |
| RouteMap::const_iterator it = route_map_.find(route_id); |
| if (it != route_map_.end()) |
| return it->second; |
| return NULL; |
| } |
| |
| void SendResponse(const std::string& name, |
| CefRefPtr<CefDictionaryValue> payload) { |
| CefRefPtr<CefDictionaryValue> result = CefDictionaryValue::Create(); |
| result->SetString(kNameKey, name); |
| result->SetDictionary(kPayloadKey, payload); |
| SendSuccess(subscription_callback_, result); |
| } |
| |
| void SendSinksResponse() { |
| CefRefPtr<CefDictionaryValue> payload = CefDictionaryValue::Create(); |
| CefRefPtr<CefListValue> sinks_list = CefListValue::Create(); |
| sinks_list->SetSize(sink_info_map_.size()); |
| |
| SinkInfoMap::const_iterator it = sink_info_map_.begin(); |
| for (size_t idx = 0; it != sink_info_map_.end(); ++it, ++idx) { |
| const SinkInfo* info = it->second; |
| |
| CefRefPtr<CefDictionaryValue> sink_dict = CefDictionaryValue::Create(); |
| sink_dict->SetString("id", it->first); |
| sink_dict->SetString("name", info->sink->GetName()); |
| sink_dict->SetString("desc", info->sink->GetDescription()); |
| sink_dict->SetInt("icon", info->sink->GetIconType()); |
| sink_dict->SetString("ip_address", |
| CefString(&info->device_info.ip_address)); |
| sink_dict->SetInt("port", info->device_info.port); |
| sink_dict->SetString("model_name", |
| CefString(&info->device_info.model_name)); |
| sink_dict->SetString("type", |
| info->sink->IsCastSink() |
| ? "cast" |
| : info->sink->IsDialSink() ? "dial" : "unknown"); |
| sinks_list->SetDictionary(idx, sink_dict); |
| } |
| |
| payload->SetList("sinks_list", sinks_list); |
| SendResponse("onSinks", payload); |
| } |
| |
| CefRefPtr<CefMediaRouter> media_router_; |
| CefRefPtr<CallbackType> subscription_callback_; |
| |
| struct SinkInfo { |
| CefRefPtr<CefMediaSink> sink; |
| CefMediaSinkDeviceInfo device_info; |
| }; |
| typedef std::map<std::string, SinkInfo*> SinkInfoMap; |
| |
| // Used to uniquely identify a call to OnSinks(), for the purpose of |
| // associating OnMediaSinkDeviceInfo() callbacks. |
| int next_sink_query_id_; |
| |
| // State from the most recent call to OnSinks(). |
| SinkInfoMap sink_info_map_; |
| int pending_sink_query_id_; |
| size_t pending_sink_callbacks_; |
| |
| // State from the most recent call to OnRoutes(). |
| typedef std::map<std::string, CefRefPtr<CefMediaRoute>> RouteMap; |
| RouteMap route_map_; |
| |
| IMPLEMENT_REFCOUNTING(MediaObserver); |
| DISALLOW_COPY_AND_ASSIGN(MediaObserver); |
| }; |
| |
| // Handle messages in the browser process. Only accessed on the UI thread. |
| class Handler : public CefMessageRouterBrowserSide::Handler { |
| public: |
| typedef std::vector<std::string> NameVector; |
| |
| Handler() { CEF_REQUIRE_UI_THREAD(); } |
| |
| virtual ~Handler() { |
| SubscriptionStateMap::iterator it = subscription_state_map_.begin(); |
| for (; it != subscription_state_map_.end(); ++it) { |
| delete it->second; |
| } |
| } |
| |
| // Called due to cefQuery execution in media_router.html. |
| bool OnQuery(CefRefPtr<CefBrowser> browser, |
| CefRefPtr<CefFrame> frame, |
| int64 query_id, |
| const CefString& request, |
| bool persistent, |
| CefRefPtr<Callback> callback) OVERRIDE { |
| CEF_REQUIRE_UI_THREAD(); |
| |
| // Only handle messages from the test URL. |
| const std::string& url = frame->GetURL(); |
| if (!test_runner::IsTestURL(url, kTestUrlPath)) |
| return false; |
| |
| // Parse |request| as a JSON dictionary. |
| CefRefPtr<CefDictionaryValue> request_dict = ParseJSON(request); |
| if (!request_dict) { |
| SendFailure(callback, kMessageFormatError, "Incorrect message format"); |
| return true; |
| } |
| |
| // Verify the "name" key. |
| if (!VerifyKey(request_dict, kNameKey, VTYPE_STRING, callback)) |
| return true; |
| |
| const std::string& message_name = request_dict->GetString(kNameKey); |
| if (message_name == kNameValueSubscribe) { |
| // Subscribe to notifications from the media router. |
| |
| if (!persistent) { |
| SendFailure(callback, kMessageFormatError, |
| "Subscriptions must be persistent"); |
| return true; |
| } |
| |
| if (!CreateSubscription(browser, query_id, callback)) { |
| SendFailure(callback, kRequestFailedError, |
| "Browser is already subscribed"); |
| } |
| return true; |
| } |
| |
| // All other messages require a current subscription. |
| CefRefPtr<MediaObserver> media_observer = |
| GetMediaObserver(browser->GetIdentifier()); |
| if (!media_observer) { |
| SendFailure(callback, kRequestFailedError, |
| "Browser is not currently subscribed"); |
| } |
| |
| if (message_name == kNameValueCreateRoute) { |
| // Create a new route. |
| |
| // Verify the "source_urn" key. |
| if (!VerifyKey(request_dict, kSourceKey, VTYPE_STRING, callback)) |
| return true; |
| // Verify the "sink_id" key. |
| if (!VerifyKey(request_dict, kSinkKey, VTYPE_STRING, callback)) |
| return true; |
| |
| const std::string& source_urn = request_dict->GetString(kSourceKey); |
| const std::string& sink_id = request_dict->GetString(kSinkKey); |
| |
| // |callback| will be executed once the route is created. |
| std::string error; |
| if (!media_observer->CreateRoute(source_urn, sink_id, callback, error)) { |
| SendFailure(callback, kRequestFailedError, error); |
| } |
| return true; |
| } else if (message_name == kNameValueTerminateRoute) { |
| // Terminate an existing route. |
| |
| // Verify the "route" key. |
| if (!VerifyKey(request_dict, kRouteKey, VTYPE_STRING, callback)) |
| return true; |
| |
| const std::string& route_id = request_dict->GetString(kRouteKey); |
| std::string error; |
| if (!media_observer->TerminateRoute(route_id, error)) { |
| SendFailure(callback, kRequestFailedError, error); |
| } else { |
| SendSuccessACK(callback); |
| } |
| return true; |
| } else if (message_name == kNameValueSendMessage) { |
| // Send a route message. |
| |
| // Verify the "route_id" key. |
| if (!VerifyKey(request_dict, kRouteKey, VTYPE_STRING, callback)) |
| return true; |
| // Verify the "message" key. |
| if (!VerifyKey(request_dict, kMessageKey, VTYPE_STRING, callback)) |
| return true; |
| |
| const std::string& route_id = request_dict->GetString(kRouteKey); |
| const std::string& message = request_dict->GetString(kMessageKey); |
| std::string error; |
| if (!media_observer->SendRouteMessage(route_id, message, error)) { |
| SendFailure(callback, kRequestFailedError, error); |
| } else { |
| SendSuccessACK(callback); |
| } |
| return true; |
| } |
| |
| return false; |
| } |
| |
| void OnQueryCanceled(CefRefPtr<CefBrowser> browser, |
| CefRefPtr<CefFrame> frame, |
| int64 query_id) OVERRIDE { |
| CEF_REQUIRE_UI_THREAD(); |
| RemoveSubscription(browser->GetIdentifier(), query_id); |
| } |
| |
| private: |
| static void SendSuccessACK(CefRefPtr<Callback> callback) { |
| CefRefPtr<CefDictionaryValue> result = CefDictionaryValue::Create(); |
| result->SetBool(kSuccessKey, true); |
| SendSuccess(callback, result); |
| } |
| |
| // Convert a JSON string to a dictionary value. |
| static CefRefPtr<CefDictionaryValue> ParseJSON(const CefString& string) { |
| CefRefPtr<CefValue> value = CefParseJSON(string, JSON_PARSER_RFC); |
| if (value.get() && value->GetType() == VTYPE_DICTIONARY) |
| return value->GetDictionary(); |
| return nullptr; |
| } |
| |
| // Verify that |key| exists in |dictionary| and has type |value_type|. Fails |
| // |callback| and returns false on failure. |
| static bool VerifyKey(CefRefPtr<CefDictionaryValue> dictionary, |
| const char* key, |
| cef_value_type_t value_type, |
| CefRefPtr<Callback> callback) { |
| if (!dictionary->HasKey(key) || dictionary->GetType(key) != value_type) { |
| SendFailure( |
| callback, kMessageFormatError, |
| "Missing or incorrectly formatted message key: " + std::string(key)); |
| return false; |
| } |
| return true; |
| } |
| |
| // Subscription state associated with a single browser. |
| struct SubscriptionState { |
| int64 query_id; |
| CefRefPtr<MediaObserver> observer; |
| CefRefPtr<CefRegistration> registration; |
| }; |
| |
| bool CreateSubscription(CefRefPtr<CefBrowser> browser, |
| int64 query_id, |
| CefRefPtr<Callback> callback) { |
| const int browser_id = browser->GetIdentifier(); |
| if (subscription_state_map_.find(browser_id) != |
| subscription_state_map_.end()) { |
| // An subscription already exists for this browser. |
| return false; |
| } |
| |
| CefRefPtr<CefMediaRouter> media_router = |
| browser->GetHost()->GetRequestContext()->GetMediaRouter(); |
| |
| SubscriptionState* state = new SubscriptionState(); |
| state->query_id = query_id; |
| state->observer = new MediaObserver(media_router, callback); |
| state->registration = media_router->AddObserver(state->observer); |
| subscription_state_map_.insert(std::make_pair(browser_id, state)); |
| |
| // Trigger sink and route callbacks. |
| media_router->NotifyCurrentSinks(); |
| media_router->NotifyCurrentRoutes(); |
| |
| return true; |
| } |
| |
| void RemoveSubscription(int browser_id, int64 query_id) { |
| SubscriptionStateMap::iterator it = |
| subscription_state_map_.find(browser_id); |
| if (it != subscription_state_map_.end() && |
| it->second->query_id == query_id) { |
| delete it->second; |
| subscription_state_map_.erase(it); |
| } |
| } |
| |
| CefRefPtr<MediaObserver> GetMediaObserver(int browser_id) { |
| SubscriptionStateMap::const_iterator it = |
| subscription_state_map_.find(browser_id); |
| if (it != subscription_state_map_.end()) { |
| return it->second->observer; |
| } |
| return NULL; |
| } |
| |
| // Map of browser ID to SubscriptionState object. |
| typedef std::map<int, SubscriptionState*> SubscriptionStateMap; |
| SubscriptionStateMap subscription_state_map_; |
| |
| DISALLOW_COPY_AND_ASSIGN(Handler); |
| }; |
| |
| } // namespace |
| |
| void CreateMessageHandlers(test_runner::MessageHandlerSet& handlers) { |
| handlers.insert(new Handler()); |
| } |
| |
| } // namespace media_router_test |
| } // namespace client |