• 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_ = nullptr;
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     using CallbackType =
153         base::OnceCallback<void(const std::string& sink_id,
154                                 const CefMediaSinkDeviceInfo& device_info)>;
155 
DeviceInfoCallback(const std::string & sink_id,CallbackType callback)156     DeviceInfoCallback(const std::string& sink_id, CallbackType callback)
157         : sink_id_(sink_id), callback_(std::move(callback)) {}
158 
OnMediaSinkDeviceInfo(const CefMediaSinkDeviceInfo & device_info)159     void OnMediaSinkDeviceInfo(
160         const CefMediaSinkDeviceInfo& device_info) override {
161       CEF_REQUIRE_UI_THREAD();
162       std::move(callback_).Run(sink_id_, device_info);
163     }
164 
165    private:
166     const std::string sink_id_;
167     CallbackType callback_;
168 
169     IMPLEMENT_REFCOUNTING(DeviceInfoCallback);
170     DISALLOW_COPY_AND_ASSIGN(DeviceInfoCallback);
171   };
172 
173   // CefMediaObserver methods:
OnSinks(const MediaSinkVector & sinks)174   void OnSinks(const MediaSinkVector& sinks) override {
175     CEF_REQUIRE_UI_THREAD();
176 
177     ClearSinkInfoMap();
178 
179     // Reset pending sink state.
180     pending_sink_callbacks_ = sinks.size();
181     pending_sink_query_id_ = ++next_sink_query_id_;
182 
183     if (sinks.empty()) {
184       // No sinks, send the response immediately.
185       SendSinksResponse();
186       return;
187     }
188 
189     MediaSinkVector::const_iterator it = sinks.begin();
190     for (size_t idx = 0; it != sinks.end(); ++it, ++idx) {
191       CefRefPtr<CefMediaSink> sink = *it;
192       const std::string& sink_id = sink->GetId();
193       SinkInfo* info = new SinkInfo;
194       info->sink = sink;
195       sink_info_map_.insert(std::make_pair(sink_id, info));
196 
197       // Request the device info asynchronously. Send the response once all
198       // callbacks have executed.
199       auto callback = base::BindOnce(&MediaObserver::OnSinkDeviceInfo, this,
200                                      pending_sink_query_id_);
201       sink->GetDeviceInfo(new DeviceInfoCallback(sink_id, std::move(callback)));
202     }
203   }
204 
OnRoutes(const MediaRouteVector & routes)205   void OnRoutes(const MediaRouteVector& routes) override {
206     CEF_REQUIRE_UI_THREAD();
207 
208     route_map_.clear();
209 
210     CefRefPtr<CefDictionaryValue> payload = CefDictionaryValue::Create();
211     CefRefPtr<CefListValue> routes_list = CefListValue::Create();
212     routes_list->SetSize(routes.size());
213 
214     MediaRouteVector::const_iterator it = routes.begin();
215     for (size_t idx = 0; it != routes.end(); ++it, ++idx) {
216       CefRefPtr<CefMediaRoute> route = *it;
217       const std::string& route_id = route->GetId();
218       route_map_.insert(std::make_pair(route_id, route));
219 
220       CefRefPtr<CefDictionaryValue> route_dict = CefDictionaryValue::Create();
221       route_dict->SetString("id", route_id);
222       route_dict->SetString(kSourceKey, route->GetSource()->GetId());
223       route_dict->SetString(kSinkKey, route->GetSink()->GetId());
224       routes_list->SetDictionary(idx, route_dict);
225     }
226 
227     payload->SetList("routes_list", routes_list);
228     SendResponse("onRoutes", payload);
229   }
230 
OnRouteStateChanged(CefRefPtr<CefMediaRoute> route,ConnectionState state)231   void OnRouteStateChanged(CefRefPtr<CefMediaRoute> route,
232                            ConnectionState state) override {
233     CEF_REQUIRE_UI_THREAD();
234 
235     CefRefPtr<CefDictionaryValue> payload = CefDictionaryValue::Create();
236     payload->SetString(kRouteKey, route->GetId());
237     payload->SetInt("connection_state", state);
238     SendResponse("onRouteStateChanged", payload);
239   }
240 
OnRouteMessageReceived(CefRefPtr<CefMediaRoute> route,const void * message,size_t message_size)241   void OnRouteMessageReceived(CefRefPtr<CefMediaRoute> route,
242                               const void* message,
243                               size_t message_size) override {
244     CEF_REQUIRE_UI_THREAD();
245 
246     std::string message_str(static_cast<const char*>(message), message_size);
247 
248     CefRefPtr<CefDictionaryValue> payload = CefDictionaryValue::Create();
249     payload->SetString(kRouteKey, route->GetId());
250     payload->SetString(kMessageKey, message_str);
251     SendResponse("onRouteMessageReceived", payload);
252   }
253 
254  private:
GetSource(const std::string & source_urn)255   CefRefPtr<CefMediaSource> GetSource(const std::string& source_urn) {
256     CefRefPtr<CefMediaSource> source = media_router_->GetSource(source_urn);
257     if (!source)
258       return nullptr;
259     return source;
260   }
261 
GetSink(const std::string & sink_id)262   CefRefPtr<CefMediaSink> GetSink(const std::string& sink_id) {
263     SinkInfoMap::const_iterator it = sink_info_map_.find(sink_id);
264     if (it != sink_info_map_.end())
265       return it->second->sink;
266     return nullptr;
267   }
268 
ClearSinkInfoMap()269   void ClearSinkInfoMap() {
270     SinkInfoMap::const_iterator it = sink_info_map_.begin();
271     for (; it != sink_info_map_.end(); ++it) {
272       delete it->second;
273     }
274     sink_info_map_.clear();
275   }
276 
OnSinkDeviceInfo(int sink_query_id,const std::string & sink_id,const CefMediaSinkDeviceInfo & device_info)277   void OnSinkDeviceInfo(int sink_query_id,
278                         const std::string& sink_id,
279                         const CefMediaSinkDeviceInfo& device_info) {
280     // Discard callbacks that arrive after a new call to OnSinks().
281     if (sink_query_id != pending_sink_query_id_)
282       return;
283 
284     SinkInfoMap::const_iterator it = sink_info_map_.find(sink_id);
285     if (it != sink_info_map_.end()) {
286       it->second->device_info = device_info;
287     }
288 
289     // Send the response once we've received all expected callbacks.
290     DCHECK_GT(pending_sink_callbacks_, 0U);
291     if (--pending_sink_callbacks_ == 0U) {
292       SendSinksResponse();
293     }
294   }
295 
GetRoute(const std::string & route_id)296   CefRefPtr<CefMediaRoute> GetRoute(const std::string& route_id) {
297     RouteMap::const_iterator it = route_map_.find(route_id);
298     if (it != route_map_.end())
299       return it->second;
300     return nullptr;
301   }
302 
SendResponse(const std::string & name,CefRefPtr<CefDictionaryValue> payload)303   void SendResponse(const std::string& name,
304                     CefRefPtr<CefDictionaryValue> payload) {
305     CefRefPtr<CefDictionaryValue> result = CefDictionaryValue::Create();
306     result->SetString(kNameKey, name);
307     result->SetDictionary(kPayloadKey, payload);
308     SendSuccess(subscription_callback_, result);
309   }
310 
SendSinksResponse()311   void SendSinksResponse() {
312     CefRefPtr<CefDictionaryValue> payload = CefDictionaryValue::Create();
313     CefRefPtr<CefListValue> sinks_list = CefListValue::Create();
314     sinks_list->SetSize(sink_info_map_.size());
315 
316     SinkInfoMap::const_iterator it = sink_info_map_.begin();
317     for (size_t idx = 0; it != sink_info_map_.end(); ++it, ++idx) {
318       const SinkInfo* info = it->second;
319 
320       CefRefPtr<CefDictionaryValue> sink_dict = CefDictionaryValue::Create();
321       sink_dict->SetString("id", it->first);
322       sink_dict->SetString("name", info->sink->GetName());
323       sink_dict->SetString("desc", info->sink->GetDescription());
324       sink_dict->SetInt("icon", info->sink->GetIconType());
325       sink_dict->SetString("ip_address",
326                            CefString(&info->device_info.ip_address));
327       sink_dict->SetInt("port", info->device_info.port);
328       sink_dict->SetString("model_name",
329                            CefString(&info->device_info.model_name));
330       sink_dict->SetString("type",
331                            info->sink->IsCastSink()
332                                ? "cast"
333                                : info->sink->IsDialSink() ? "dial" : "unknown");
334       sinks_list->SetDictionary(idx, sink_dict);
335     }
336 
337     payload->SetList("sinks_list", sinks_list);
338     SendResponse("onSinks", payload);
339   }
340 
341   CefRefPtr<CefMediaRouter> media_router_;
342   CefRefPtr<CallbackType> subscription_callback_;
343 
344   struct SinkInfo {
345     CefRefPtr<CefMediaSink> sink;
346     CefMediaSinkDeviceInfo device_info;
347   };
348   typedef std::map<std::string, SinkInfo*> SinkInfoMap;
349 
350   // Used to uniquely identify a call to OnSinks(), for the purpose of
351   // associating OnMediaSinkDeviceInfo() callbacks.
352   int next_sink_query_id_;
353 
354   // State from the most recent call to OnSinks().
355   SinkInfoMap sink_info_map_;
356   int pending_sink_query_id_;
357   size_t pending_sink_callbacks_;
358 
359   // State from the most recent call to OnRoutes().
360   typedef std::map<std::string, CefRefPtr<CefMediaRoute>> RouteMap;
361   RouteMap route_map_;
362 
363   IMPLEMENT_REFCOUNTING(MediaObserver);
364   DISALLOW_COPY_AND_ASSIGN(MediaObserver);
365 };
366 
367 // Handle messages in the browser process. Only accessed on the UI thread.
368 class Handler : public CefMessageRouterBrowserSide::Handler {
369  public:
370   typedef std::vector<std::string> NameVector;
371 
Handler()372   Handler() { CEF_REQUIRE_UI_THREAD(); }
373 
~Handler()374   virtual ~Handler() {
375     SubscriptionStateMap::iterator it = subscription_state_map_.begin();
376     for (; it != subscription_state_map_.end(); ++it) {
377       delete it->second;
378     }
379   }
380 
381   // 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)382   bool OnQuery(CefRefPtr<CefBrowser> browser,
383                CefRefPtr<CefFrame> frame,
384                int64 query_id,
385                const CefString& request,
386                bool persistent,
387                CefRefPtr<Callback> callback) override {
388     CEF_REQUIRE_UI_THREAD();
389 
390     // Only handle messages from the test URL.
391     const std::string& url = frame->GetURL();
392     if (!test_runner::IsTestURL(url, kTestUrlPath))
393       return false;
394 
395     // Parse |request| as a JSON dictionary.
396     CefRefPtr<CefDictionaryValue> request_dict = ParseJSON(request);
397     if (!request_dict) {
398       SendFailure(callback, kMessageFormatError, "Incorrect message format");
399       return true;
400     }
401 
402     // Verify the "name" key.
403     if (!VerifyKey(request_dict, kNameKey, VTYPE_STRING, callback))
404       return true;
405 
406     const std::string& message_name = request_dict->GetString(kNameKey);
407     if (message_name == kNameValueSubscribe) {
408       // Subscribe to notifications from the media router.
409 
410       if (!persistent) {
411         SendFailure(callback, kMessageFormatError,
412                     "Subscriptions must be persistent");
413         return true;
414       }
415 
416       if (!CreateSubscription(browser, query_id, callback)) {
417         SendFailure(callback, kRequestFailedError,
418                     "Browser is already subscribed");
419       }
420       return true;
421     }
422 
423     // All other messages require a current subscription.
424     CefRefPtr<MediaObserver> media_observer =
425         GetMediaObserver(browser->GetIdentifier());
426     if (!media_observer) {
427       SendFailure(callback, kRequestFailedError,
428                   "Browser is not currently subscribed");
429     }
430 
431     if (message_name == kNameValueCreateRoute) {
432       // Create a new route.
433 
434       // Verify the "source_urn" key.
435       if (!VerifyKey(request_dict, kSourceKey, VTYPE_STRING, callback))
436         return true;
437       // Verify the "sink_id" key.
438       if (!VerifyKey(request_dict, kSinkKey, VTYPE_STRING, callback))
439         return true;
440 
441       const std::string& source_urn = request_dict->GetString(kSourceKey);
442       const std::string& sink_id = request_dict->GetString(kSinkKey);
443 
444       // |callback| will be executed once the route is created.
445       std::string error;
446       if (!media_observer->CreateRoute(source_urn, sink_id, callback, error)) {
447         SendFailure(callback, kRequestFailedError, error);
448       }
449       return true;
450     } else if (message_name == kNameValueTerminateRoute) {
451       // Terminate an existing route.
452 
453       // Verify the "route" key.
454       if (!VerifyKey(request_dict, kRouteKey, VTYPE_STRING, callback))
455         return true;
456 
457       const std::string& route_id = request_dict->GetString(kRouteKey);
458       std::string error;
459       if (!media_observer->TerminateRoute(route_id, error)) {
460         SendFailure(callback, kRequestFailedError, error);
461       } else {
462         SendSuccessACK(callback);
463       }
464       return true;
465     } else if (message_name == kNameValueSendMessage) {
466       // Send a route message.
467 
468       // Verify the "route_id" key.
469       if (!VerifyKey(request_dict, kRouteKey, VTYPE_STRING, callback))
470         return true;
471       // Verify the "message" key.
472       if (!VerifyKey(request_dict, kMessageKey, VTYPE_STRING, callback))
473         return true;
474 
475       const std::string& route_id = request_dict->GetString(kRouteKey);
476       const std::string& message = request_dict->GetString(kMessageKey);
477       std::string error;
478       if (!media_observer->SendRouteMessage(route_id, message, error)) {
479         SendFailure(callback, kRequestFailedError, error);
480       } else {
481         SendSuccessACK(callback);
482       }
483       return true;
484     }
485 
486     return false;
487   }
488 
OnQueryCanceled(CefRefPtr<CefBrowser> browser,CefRefPtr<CefFrame> frame,int64 query_id)489   void OnQueryCanceled(CefRefPtr<CefBrowser> browser,
490                        CefRefPtr<CefFrame> frame,
491                        int64 query_id) override {
492     CEF_REQUIRE_UI_THREAD();
493     RemoveSubscription(browser->GetIdentifier(), query_id);
494   }
495 
496  private:
SendSuccessACK(CefRefPtr<Callback> callback)497   static void SendSuccessACK(CefRefPtr<Callback> callback) {
498     CefRefPtr<CefDictionaryValue> result = CefDictionaryValue::Create();
499     result->SetBool(kSuccessKey, true);
500     SendSuccess(callback, result);
501   }
502 
503   // Convert a JSON string to a dictionary value.
ParseJSON(const CefString & string)504   static CefRefPtr<CefDictionaryValue> ParseJSON(const CefString& string) {
505     CefRefPtr<CefValue> value = CefParseJSON(string, JSON_PARSER_RFC);
506     if (value.get() && value->GetType() == VTYPE_DICTIONARY)
507       return value->GetDictionary();
508     return nullptr;
509   }
510 
511   // Verify that |key| exists in |dictionary| and has type |value_type|. Fails
512   // |callback| and returns false on failure.
VerifyKey(CefRefPtr<CefDictionaryValue> dictionary,const char * key,cef_value_type_t value_type,CefRefPtr<Callback> callback)513   static bool VerifyKey(CefRefPtr<CefDictionaryValue> dictionary,
514                         const char* key,
515                         cef_value_type_t value_type,
516                         CefRefPtr<Callback> callback) {
517     if (!dictionary->HasKey(key) || dictionary->GetType(key) != value_type) {
518       SendFailure(
519           callback, kMessageFormatError,
520           "Missing or incorrectly formatted message key: " + std::string(key));
521       return false;
522     }
523     return true;
524   }
525 
526   // Subscription state associated with a single browser.
527   struct SubscriptionState {
528     int64 query_id;
529     CefRefPtr<MediaObserver> observer;
530     CefRefPtr<CefRegistration> registration;
531   };
532 
CreateSubscription(CefRefPtr<CefBrowser> browser,int64 query_id,CefRefPtr<Callback> callback)533   bool CreateSubscription(CefRefPtr<CefBrowser> browser,
534                           int64 query_id,
535                           CefRefPtr<Callback> callback) {
536     const int browser_id = browser->GetIdentifier();
537     if (subscription_state_map_.find(browser_id) !=
538         subscription_state_map_.end()) {
539       // An subscription already exists for this browser.
540       return false;
541     }
542 
543     CefRefPtr<CefMediaRouter> media_router =
544         browser->GetHost()->GetRequestContext()->GetMediaRouter(nullptr);
545 
546     SubscriptionState* state = new SubscriptionState();
547     state->query_id = query_id;
548     state->observer = new MediaObserver(media_router, callback);
549     state->registration = media_router->AddObserver(state->observer);
550     subscription_state_map_.insert(std::make_pair(browser_id, state));
551 
552     // Trigger sink and route callbacks.
553     media_router->NotifyCurrentSinks();
554     media_router->NotifyCurrentRoutes();
555 
556     return true;
557   }
558 
RemoveSubscription(int browser_id,int64 query_id)559   void RemoveSubscription(int browser_id, int64 query_id) {
560     SubscriptionStateMap::iterator it =
561         subscription_state_map_.find(browser_id);
562     if (it != subscription_state_map_.end() &&
563         it->second->query_id == query_id) {
564       delete it->second;
565       subscription_state_map_.erase(it);
566     }
567   }
568 
GetMediaObserver(int browser_id)569   CefRefPtr<MediaObserver> GetMediaObserver(int browser_id) {
570     SubscriptionStateMap::const_iterator it =
571         subscription_state_map_.find(browser_id);
572     if (it != subscription_state_map_.end()) {
573       return it->second->observer;
574     }
575     return nullptr;
576   }
577 
578   // Map of browser ID to SubscriptionState object.
579   typedef std::map<int, SubscriptionState*> SubscriptionStateMap;
580   SubscriptionStateMap subscription_state_map_;
581 
582   DISALLOW_COPY_AND_ASSIGN(Handler);
583 };
584 
585 }  // namespace
586 
CreateMessageHandlers(test_runner::MessageHandlerSet & handlers)587 void CreateMessageHandlers(test_runner::MessageHandlerSet& handlers) {
588   handlers.insert(new Handler());
589 }
590 
591 }  // namespace media_router_test
592 }  // namespace client
593