• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright (c) 2020 The Chromium Embedded Framework Authors. All rights
2 // reserved. Use of this source code is governed by a BSD-style license that
3 // can be found in the LICENSE file.
4 
5 #include "tests/cefclient/browser/media_router_test.h"
6 
7 #include <string>
8 #include <vector>
9 
10 #include "include/base/cef_logging.h"
11 #include "include/cef_media_router.h"
12 #include "include/cef_parser.h"
13 #include "tests/cefclient/browser/test_runner.h"
14 
15 namespace client {
16 namespace media_router_test {
17 
18 namespace {
19 
20 const char kTestUrlPath[] = "/media_router";
21 
22 // Application-specific error codes.
23 const int kMessageFormatError = 1;
24 const int kRequestFailedError = 2;
25 
26 // Message strings.
27 const char kNameKey[] = "name";
28 const char kNameValueSubscribe[] = "subscribe";
29 const char kNameValueCreateRoute[] = "createRoute";
30 const char kNameValueTerminateRoute[] = "terminateRoute";
31 const char kNameValueSendMessage[] = "sendMessage";
32 const char kSourceKey[] = "source_urn";
33 const char kSinkKey[] = "sink_id";
34 const char kRouteKey[] = "route_id";
35 const char kMessageKey[] = "message";
36 const char kSuccessKey[] = "success";
37 const char kPayloadKey[] = "payload";
38 
39 // Convert a dictionary value to a JSON string.
GetJSON(CefRefPtr<CefDictionaryValue> dictionary)40 CefString GetJSON(CefRefPtr<CefDictionaryValue> dictionary) {
41   CefRefPtr<CefValue> value = CefValue::Create();
42   value->SetDictionary(dictionary);
43   return CefWriteJSON(value, JSON_WRITER_DEFAULT);
44 }
45 
46 typedef CefMessageRouterBrowserSide::Callback CallbackType;
47 
SendSuccess(CefRefPtr<CallbackType> callback,CefRefPtr<CefDictionaryValue> result)48 void SendSuccess(CefRefPtr<CallbackType> callback,
49                  CefRefPtr<CefDictionaryValue> result) {
50   callback->Success(GetJSON(result));
51 }
52 
SendFailure(CefRefPtr<CallbackType> callback,int error_code,const std::string & error_message)53 void SendFailure(CefRefPtr<CallbackType> callback,
54                  int error_code,
55                  const std::string& error_message) {
56   callback->Failure(error_code, error_message);
57 }
58 
59 // Callback for CefMediaRouter::CreateRoute.
60 class MediaRouteCreateCallback : public CefMediaRouteCreateCallback {
61  public:
MediaRouteCreateCallback(CefRefPtr<CallbackType> create_callback)62   explicit MediaRouteCreateCallback(CefRefPtr<CallbackType> create_callback)
63       : create_callback_(create_callback) {}
64 
65   // CefMediaRouteCreateCallback method:
OnMediaRouteCreateFinished(RouteCreateResult result,const CefString & error,CefRefPtr<CefMediaRoute> route)66   void OnMediaRouteCreateFinished(RouteCreateResult result,
67                                   const CefString& error,
68                                   CefRefPtr<CefMediaRoute> route) OVERRIDE {
69     CEF_REQUIRE_UI_THREAD();
70     if (result == CEF_MRCR_OK) {
71       CefRefPtr<CefDictionaryValue> dict = CefDictionaryValue::Create();
72       dict->SetString(kRouteKey, route->GetId());
73       SendSuccess(create_callback_, dict);
74     } else {
75       SendFailure(create_callback_, kRequestFailedError + result, error);
76     }
77     create_callback_ = NULL;
78   }
79 
80  private:
81   CefRefPtr<CallbackType> create_callback_;
82 
83   IMPLEMENT_REFCOUNTING(MediaRouteCreateCallback);
84   DISALLOW_COPY_AND_ASSIGN(MediaRouteCreateCallback);
85 };
86 
87 // Observes MediaRouter events. Only accessed on the UI thread.
88 class MediaObserver : public CefMediaObserver {
89  public:
90   typedef std::vector<CefRefPtr<CefMediaRoute>> MediaRouteVector;
91   typedef std::vector<CefRefPtr<CefMediaSink>> MediaSinkVector;
92 
MediaObserver(CefRefPtr<CefMediaRouter> media_router,CefRefPtr<CallbackType> subscription_callback)93   MediaObserver(CefRefPtr<CefMediaRouter> media_router,
94                 CefRefPtr<CallbackType> subscription_callback)
95       : media_router_(media_router),
96         subscription_callback_(subscription_callback),
97         next_sink_query_id_(0),
98         pending_sink_query_id_(-1),
99         pending_sink_callbacks_(0U) {}
100 
~MediaObserver()101   ~MediaObserver() OVERRIDE { ClearSinkInfoMap(); }
102 
CreateRoute(const std::string & source_urn,const std::string & sink_id,CefRefPtr<CallbackType> callback,std::string & error)103   bool CreateRoute(const std::string& source_urn,
104                    const std::string& sink_id,
105                    CefRefPtr<CallbackType> callback,
106                    std::string& error) {
107     CefRefPtr<CefMediaSource> source = GetSource(source_urn);
108     if (!source) {
109       error = "Invalid source: " + source_urn;
110       return false;
111     }
112 
113     CefRefPtr<CefMediaSink> sink = GetSink(sink_id);
114     if (!sink) {
115       error = "Invalid sink: " + sink_id;
116       return false;
117     }
118 
119     media_router_->CreateRoute(source, sink,
120                                new MediaRouteCreateCallback(callback));
121     return true;
122   }
123 
TerminateRoute(const std::string & route_id,std::string & error)124   bool TerminateRoute(const std::string& route_id, std::string& error) {
125     CefRefPtr<CefMediaRoute> route = GetRoute(route_id);
126     if (!route) {
127       error = "Invalid route: " + route_id;
128       return false;
129     }
130 
131     route->Terminate();
132     return true;
133   }
134 
SendRouteMessage(const std::string & route_id,const std::string & message,std::string & error)135   bool SendRouteMessage(const std::string& route_id,
136                         const std::string& message,
137                         std::string& error) {
138     CefRefPtr<CefMediaRoute> route = GetRoute(route_id);
139     if (!route) {
140       error = "Invalid route: " + route_id;
141       return false;
142     }
143 
144     route->SendRouteMessage(message.c_str(), message.size());
145     return true;
146   }
147 
148  protected:
149   class DeviceInfoCallback : public CefMediaSinkDeviceInfoCallback {
150    public:
151     // Callback to be executed when the device info is available.
152     typedef base::Callback<void(const std::string& sink_id,
153                                 const CefMediaSinkDeviceInfo& device_info)>
154         CallbackType;
155 
DeviceInfoCallback(const std::string & sink_id,const CallbackType & callback)156     DeviceInfoCallback(const std::string& sink_id, const CallbackType& callback)
157         : sink_id_(sink_id), callback_(callback) {}
158 
OnMediaSinkDeviceInfo(const CefMediaSinkDeviceInfo & device_info)159     void OnMediaSinkDeviceInfo(
160         const CefMediaSinkDeviceInfo& device_info) OVERRIDE {
161       CEF_REQUIRE_UI_THREAD();
162       callback_.Run(sink_id_, device_info);
163       callback_.Reset();
164     }
165 
166    private:
167     const std::string sink_id_;
168     CallbackType callback_;
169 
170     IMPLEMENT_REFCOUNTING(DeviceInfoCallback);
171     DISALLOW_COPY_AND_ASSIGN(DeviceInfoCallback);
172   };
173 
174   // CefMediaObserver methods:
OnSinks(const MediaSinkVector & sinks)175   void OnSinks(const MediaSinkVector& sinks) OVERRIDE {
176     CEF_REQUIRE_UI_THREAD();
177 
178     ClearSinkInfoMap();
179 
180     // Reset pending sink state.
181     pending_sink_callbacks_ = sinks.size();
182     pending_sink_query_id_ = ++next_sink_query_id_;
183 
184     if (sinks.empty()) {
185       // No sinks, send the response immediately.
186       SendSinksResponse();
187       return;
188     }
189 
190     DeviceInfoCallback::CallbackType callback = base::Bind(
191         &MediaObserver::OnSinkDeviceInfo, this, pending_sink_query_id_);
192 
193     MediaSinkVector::const_iterator it = sinks.begin();
194     for (size_t idx = 0; it != sinks.end(); ++it, ++idx) {
195       CefRefPtr<CefMediaSink> sink = *it;
196       const std::string& sink_id = sink->GetId();
197       SinkInfo* info = new SinkInfo;
198       info->sink = sink;
199       sink_info_map_.insert(std::make_pair(sink_id, info));
200 
201       // Request the device info asynchronously. Send the response once all
202       // callbacks have executed.
203       sink->GetDeviceInfo(new DeviceInfoCallback(sink_id, callback));
204     }
205   }
206 
OnRoutes(const MediaRouteVector & routes)207   void OnRoutes(const MediaRouteVector& routes) OVERRIDE {
208     CEF_REQUIRE_UI_THREAD();
209 
210     route_map_.clear();
211 
212     CefRefPtr<CefDictionaryValue> payload = CefDictionaryValue::Create();
213     CefRefPtr<CefListValue> routes_list = CefListValue::Create();
214     routes_list->SetSize(routes.size());
215 
216     MediaRouteVector::const_iterator it = routes.begin();
217     for (size_t idx = 0; it != routes.end(); ++it, ++idx) {
218       CefRefPtr<CefMediaRoute> route = *it;
219       const std::string& route_id = route->GetId();
220       route_map_.insert(std::make_pair(route_id, route));
221 
222       CefRefPtr<CefDictionaryValue> route_dict = CefDictionaryValue::Create();
223       route_dict->SetString("id", route_id);
224       route_dict->SetString(kSourceKey, route->GetSource()->GetId());
225       route_dict->SetString(kSinkKey, route->GetSink()->GetId());
226       routes_list->SetDictionary(idx, route_dict);
227     }
228 
229     payload->SetList("routes_list", routes_list);
230     SendResponse("onRoutes", payload);
231   }
232 
OnRouteStateChanged(CefRefPtr<CefMediaRoute> route,ConnectionState state)233   void OnRouteStateChanged(CefRefPtr<CefMediaRoute> route,
234                            ConnectionState state) OVERRIDE {
235     CEF_REQUIRE_UI_THREAD();
236 
237     CefRefPtr<CefDictionaryValue> payload = CefDictionaryValue::Create();
238     payload->SetString(kRouteKey, route->GetId());
239     payload->SetInt("connection_state", state);
240     SendResponse("onRouteStateChanged", payload);
241   }
242 
OnRouteMessageReceived(CefRefPtr<CefMediaRoute> route,const void * message,size_t message_size)243   void OnRouteMessageReceived(CefRefPtr<CefMediaRoute> route,
244                               const void* message,
245                               size_t message_size) OVERRIDE {
246     CEF_REQUIRE_UI_THREAD();
247 
248     std::string message_str(static_cast<const char*>(message), message_size);
249 
250     CefRefPtr<CefDictionaryValue> payload = CefDictionaryValue::Create();
251     payload->SetString(kRouteKey, route->GetId());
252     payload->SetString(kMessageKey, message_str);
253     SendResponse("onRouteMessageReceived", payload);
254   }
255 
256  private:
GetSource(const std::string & source_urn)257   CefRefPtr<CefMediaSource> GetSource(const std::string& source_urn) {
258     CefRefPtr<CefMediaSource> source = media_router_->GetSource(source_urn);
259     if (!source)
260       return NULL;
261     return source;
262   }
263 
GetSink(const std::string & sink_id)264   CefRefPtr<CefMediaSink> GetSink(const std::string& sink_id) {
265     SinkInfoMap::const_iterator it = sink_info_map_.find(sink_id);
266     if (it != sink_info_map_.end())
267       return it->second->sink;
268     return NULL;
269   }
270 
ClearSinkInfoMap()271   void ClearSinkInfoMap() {
272     SinkInfoMap::const_iterator it = sink_info_map_.begin();
273     for (; it != sink_info_map_.end(); ++it) {
274       delete it->second;
275     }
276     sink_info_map_.clear();
277   }
278 
OnSinkDeviceInfo(int sink_query_id,const std::string & sink_id,const CefMediaSinkDeviceInfo & device_info)279   void OnSinkDeviceInfo(int sink_query_id,
280                         const std::string& sink_id,
281                         const CefMediaSinkDeviceInfo& device_info) {
282     // Discard callbacks that arrive after a new call to OnSinks().
283     if (sink_query_id != pending_sink_query_id_)
284       return;
285 
286     SinkInfoMap::const_iterator it = sink_info_map_.find(sink_id);
287     if (it != sink_info_map_.end()) {
288       it->second->device_info = device_info;
289     }
290 
291     // Send the response once we've received all expected callbacks.
292     DCHECK_GT(pending_sink_callbacks_, 0U);
293     if (--pending_sink_callbacks_ == 0U) {
294       SendSinksResponse();
295     }
296   }
297 
GetRoute(const std::string & route_id)298   CefRefPtr<CefMediaRoute> GetRoute(const std::string& route_id) {
299     RouteMap::const_iterator it = route_map_.find(route_id);
300     if (it != route_map_.end())
301       return it->second;
302     return NULL;
303   }
304 
SendResponse(const std::string & name,CefRefPtr<CefDictionaryValue> payload)305   void SendResponse(const std::string& name,
306                     CefRefPtr<CefDictionaryValue> payload) {
307     CefRefPtr<CefDictionaryValue> result = CefDictionaryValue::Create();
308     result->SetString(kNameKey, name);
309     result->SetDictionary(kPayloadKey, payload);
310     SendSuccess(subscription_callback_, result);
311   }
312 
SendSinksResponse()313   void SendSinksResponse() {
314     CefRefPtr<CefDictionaryValue> payload = CefDictionaryValue::Create();
315     CefRefPtr<CefListValue> sinks_list = CefListValue::Create();
316     sinks_list->SetSize(sink_info_map_.size());
317 
318     SinkInfoMap::const_iterator it = sink_info_map_.begin();
319     for (size_t idx = 0; it != sink_info_map_.end(); ++it, ++idx) {
320       const SinkInfo* info = it->second;
321 
322       CefRefPtr<CefDictionaryValue> sink_dict = CefDictionaryValue::Create();
323       sink_dict->SetString("id", it->first);
324       sink_dict->SetString("name", info->sink->GetName());
325       sink_dict->SetString("desc", info->sink->GetDescription());
326       sink_dict->SetInt("icon", info->sink->GetIconType());
327       sink_dict->SetString("ip_address",
328                            CefString(&info->device_info.ip_address));
329       sink_dict->SetInt("port", info->device_info.port);
330       sink_dict->SetString("model_name",
331                            CefString(&info->device_info.model_name));
332       sink_dict->SetString("type",
333                            info->sink->IsCastSink()
334                                ? "cast"
335                                : info->sink->IsDialSink() ? "dial" : "unknown");
336       sinks_list->SetDictionary(idx, sink_dict);
337     }
338 
339     payload->SetList("sinks_list", sinks_list);
340     SendResponse("onSinks", payload);
341   }
342 
343   CefRefPtr<CefMediaRouter> media_router_;
344   CefRefPtr<CallbackType> subscription_callback_;
345 
346   struct SinkInfo {
347     CefRefPtr<CefMediaSink> sink;
348     CefMediaSinkDeviceInfo device_info;
349   };
350   typedef std::map<std::string, SinkInfo*> SinkInfoMap;
351 
352   // Used to uniquely identify a call to OnSinks(), for the purpose of
353   // associating OnMediaSinkDeviceInfo() callbacks.
354   int next_sink_query_id_;
355 
356   // State from the most recent call to OnSinks().
357   SinkInfoMap sink_info_map_;
358   int pending_sink_query_id_;
359   size_t pending_sink_callbacks_;
360 
361   // State from the most recent call to OnRoutes().
362   typedef std::map<std::string, CefRefPtr<CefMediaRoute>> RouteMap;
363   RouteMap route_map_;
364 
365   IMPLEMENT_REFCOUNTING(MediaObserver);
366   DISALLOW_COPY_AND_ASSIGN(MediaObserver);
367 };
368 
369 // Handle messages in the browser process. Only accessed on the UI thread.
370 class Handler : public CefMessageRouterBrowserSide::Handler {
371  public:
372   typedef std::vector<std::string> NameVector;
373 
Handler()374   Handler() { CEF_REQUIRE_UI_THREAD(); }
375 
~Handler()376   virtual ~Handler() {
377     SubscriptionStateMap::iterator it = subscription_state_map_.begin();
378     for (; it != subscription_state_map_.end(); ++it) {
379       delete it->second;
380     }
381   }
382 
383   // Called due to cefQuery execution in media_router.html.
OnQuery(CefRefPtr<CefBrowser> browser,CefRefPtr<CefFrame> frame,int64 query_id,const CefString & request,bool persistent,CefRefPtr<Callback> callback)384   bool OnQuery(CefRefPtr<CefBrowser> browser,
385                CefRefPtr<CefFrame> frame,
386                int64 query_id,
387                const CefString& request,
388                bool persistent,
389                CefRefPtr<Callback> callback) OVERRIDE {
390     CEF_REQUIRE_UI_THREAD();
391 
392     // Only handle messages from the test URL.
393     const std::string& url = frame->GetURL();
394     if (!test_runner::IsTestURL(url, kTestUrlPath))
395       return false;
396 
397     // Parse |request| as a JSON dictionary.
398     CefRefPtr<CefDictionaryValue> request_dict = ParseJSON(request);
399     if (!request_dict) {
400       SendFailure(callback, kMessageFormatError, "Incorrect message format");
401       return true;
402     }
403 
404     // Verify the "name" key.
405     if (!VerifyKey(request_dict, kNameKey, VTYPE_STRING, callback))
406       return true;
407 
408     const std::string& message_name = request_dict->GetString(kNameKey);
409     if (message_name == kNameValueSubscribe) {
410       // Subscribe to notifications from the media router.
411 
412       if (!persistent) {
413         SendFailure(callback, kMessageFormatError,
414                     "Subscriptions must be persistent");
415         return true;
416       }
417 
418       if (!CreateSubscription(browser, query_id, callback)) {
419         SendFailure(callback, kRequestFailedError,
420                     "Browser is already subscribed");
421       }
422       return true;
423     }
424 
425     // All other messages require a current subscription.
426     CefRefPtr<MediaObserver> media_observer =
427         GetMediaObserver(browser->GetIdentifier());
428     if (!media_observer) {
429       SendFailure(callback, kRequestFailedError,
430                   "Browser is not currently subscribed");
431     }
432 
433     if (message_name == kNameValueCreateRoute) {
434       // Create a new route.
435 
436       // Verify the "source_urn" key.
437       if (!VerifyKey(request_dict, kSourceKey, VTYPE_STRING, callback))
438         return true;
439       // Verify the "sink_id" key.
440       if (!VerifyKey(request_dict, kSinkKey, VTYPE_STRING, callback))
441         return true;
442 
443       const std::string& source_urn = request_dict->GetString(kSourceKey);
444       const std::string& sink_id = request_dict->GetString(kSinkKey);
445 
446       // |callback| will be executed once the route is created.
447       std::string error;
448       if (!media_observer->CreateRoute(source_urn, sink_id, callback, error)) {
449         SendFailure(callback, kRequestFailedError, error);
450       }
451       return true;
452     } else if (message_name == kNameValueTerminateRoute) {
453       // Terminate an existing route.
454 
455       // Verify the "route" key.
456       if (!VerifyKey(request_dict, kRouteKey, VTYPE_STRING, callback))
457         return true;
458 
459       const std::string& route_id = request_dict->GetString(kRouteKey);
460       std::string error;
461       if (!media_observer->TerminateRoute(route_id, error)) {
462         SendFailure(callback, kRequestFailedError, error);
463       } else {
464         SendSuccessACK(callback);
465       }
466       return true;
467     } else if (message_name == kNameValueSendMessage) {
468       // Send a route message.
469 
470       // Verify the "route_id" key.
471       if (!VerifyKey(request_dict, kRouteKey, VTYPE_STRING, callback))
472         return true;
473       // Verify the "message" key.
474       if (!VerifyKey(request_dict, kMessageKey, VTYPE_STRING, callback))
475         return true;
476 
477       const std::string& route_id = request_dict->GetString(kRouteKey);
478       const std::string& message = request_dict->GetString(kMessageKey);
479       std::string error;
480       if (!media_observer->SendRouteMessage(route_id, message, error)) {
481         SendFailure(callback, kRequestFailedError, error);
482       } else {
483         SendSuccessACK(callback);
484       }
485       return true;
486     }
487 
488     return false;
489   }
490 
OnQueryCanceled(CefRefPtr<CefBrowser> browser,CefRefPtr<CefFrame> frame,int64 query_id)491   void OnQueryCanceled(CefRefPtr<CefBrowser> browser,
492                        CefRefPtr<CefFrame> frame,
493                        int64 query_id) OVERRIDE {
494     CEF_REQUIRE_UI_THREAD();
495     RemoveSubscription(browser->GetIdentifier(), query_id);
496   }
497 
498  private:
SendSuccessACK(CefRefPtr<Callback> callback)499   static void SendSuccessACK(CefRefPtr<Callback> callback) {
500     CefRefPtr<CefDictionaryValue> result = CefDictionaryValue::Create();
501     result->SetBool(kSuccessKey, true);
502     SendSuccess(callback, result);
503   }
504 
505   // Convert a JSON string to a dictionary value.
ParseJSON(const CefString & string)506   static CefRefPtr<CefDictionaryValue> ParseJSON(const CefString& string) {
507     CefRefPtr<CefValue> value = CefParseJSON(string, JSON_PARSER_RFC);
508     if (value.get() && value->GetType() == VTYPE_DICTIONARY)
509       return value->GetDictionary();
510     return nullptr;
511   }
512 
513   // Verify that |key| exists in |dictionary| and has type |value_type|. Fails
514   // |callback| and returns false on failure.
VerifyKey(CefRefPtr<CefDictionaryValue> dictionary,const char * key,cef_value_type_t value_type,CefRefPtr<Callback> callback)515   static bool VerifyKey(CefRefPtr<CefDictionaryValue> dictionary,
516                         const char* key,
517                         cef_value_type_t value_type,
518                         CefRefPtr<Callback> callback) {
519     if (!dictionary->HasKey(key) || dictionary->GetType(key) != value_type) {
520       SendFailure(
521           callback, kMessageFormatError,
522           "Missing or incorrectly formatted message key: " + std::string(key));
523       return false;
524     }
525     return true;
526   }
527 
528   // Subscription state associated with a single browser.
529   struct SubscriptionState {
530     int64 query_id;
531     CefRefPtr<MediaObserver> observer;
532     CefRefPtr<CefRegistration> registration;
533   };
534 
CreateSubscription(CefRefPtr<CefBrowser> browser,int64 query_id,CefRefPtr<Callback> callback)535   bool CreateSubscription(CefRefPtr<CefBrowser> browser,
536                           int64 query_id,
537                           CefRefPtr<Callback> callback) {
538     const int browser_id = browser->GetIdentifier();
539     if (subscription_state_map_.find(browser_id) !=
540         subscription_state_map_.end()) {
541       // An subscription already exists for this browser.
542       return false;
543     }
544 
545     CefRefPtr<CefMediaRouter> media_router =
546         browser->GetHost()->GetRequestContext()->GetMediaRouter(nullptr);
547 
548     SubscriptionState* state = new SubscriptionState();
549     state->query_id = query_id;
550     state->observer = new MediaObserver(media_router, callback);
551     state->registration = media_router->AddObserver(state->observer);
552     subscription_state_map_.insert(std::make_pair(browser_id, state));
553 
554     // Trigger sink and route callbacks.
555     media_router->NotifyCurrentSinks();
556     media_router->NotifyCurrentRoutes();
557 
558     return true;
559   }
560 
RemoveSubscription(int browser_id,int64 query_id)561   void RemoveSubscription(int browser_id, int64 query_id) {
562     SubscriptionStateMap::iterator it =
563         subscription_state_map_.find(browser_id);
564     if (it != subscription_state_map_.end() &&
565         it->second->query_id == query_id) {
566       delete it->second;
567       subscription_state_map_.erase(it);
568     }
569   }
570 
GetMediaObserver(int browser_id)571   CefRefPtr<MediaObserver> GetMediaObserver(int browser_id) {
572     SubscriptionStateMap::const_iterator it =
573         subscription_state_map_.find(browser_id);
574     if (it != subscription_state_map_.end()) {
575       return it->second->observer;
576     }
577     return NULL;
578   }
579 
580   // Map of browser ID to SubscriptionState object.
581   typedef std::map<int, SubscriptionState*> SubscriptionStateMap;
582   SubscriptionStateMap subscription_state_map_;
583 
584   DISALLOW_COPY_AND_ASSIGN(Handler);
585 };
586 
587 }  // namespace
588 
CreateMessageHandlers(test_runner::MessageHandlerSet & handlers)589 void CreateMessageHandlers(test_runner::MessageHandlerSet& handlers) {
590   handlers.insert(new Handler());
591 }
592 
593 }  // namespace media_router_test
594 }  // namespace client
595