Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(533)

Side by Side Diff: chrome/browser/media/cast_remoting_connector.cc

Issue 2310753002: Media Remoting: Data/Control plumbing between renderer and Media Router. (Closed)
Patch Set: Updated/Moved TODO comment in render_frame_impl.cc. And REBASE. Created 4 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 // Copyright 2016 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "chrome/browser/media/cast_remoting_connector.h"
6
7 #include <stdio.h>
8
9 #include "base/bind.h"
10 #include "base/bind_helpers.h"
11 #include "base/callback.h"
12 #include "base/logging.h"
13 #include "base/memory/ptr_util.h"
14 #include "base/strings/string_number_conversions.h"
15 #include "base/strings/string_piece.h"
16 #include "base/strings/stringprintf.h"
17 #include "chrome/browser/media/cast_remoting_sender.h"
18 #include "chrome/browser/media/router/media_router.h"
19 #include "chrome/browser/media/router/media_router_factory.h"
20 #include "chrome/browser/media/router/media_source_helper.h"
21 #include "chrome/browser/media/router/route_message.h"
22 #include "chrome/browser/media/router/route_message_observer.h"
23 #include "content/public/browser/browser_thread.h"
24 #include "mojo/public/cpp/bindings/strong_binding.h"
25
26 DEFINE_WEB_CONTENTS_USER_DATA_KEY(CastRemotingConnector);
27
28 using content::BrowserThread;
29 using media::mojom::RemotingStartFailReason;
30 using media::mojom::RemotingStopReason;
31
32 namespace {
33
34 // Simple command messages sent from/to the connector to/from the Media Router
35 // Cast Provider to start/stop media remoting to a Cast device.
36 //
37 // Field separator (for tokenizing parts of messages).
38 constexpr char kMessageFieldSeparator = ':';
39 // Message sent by CastRemotingConnector to Cast provider to start remoting.
40 // Example:
41 // "START_CAST_REMOTING:session=1f"
42 constexpr char kStartRemotingMessageFormat[] =
43 "START_CAST_REMOTING:session=%x";
44 // Message sent by CastRemotingConnector to Cast provider to start the remoting
45 // RTP stream(s). Example:
46 // "START_CAST_REMOTING_STREAMS:session=1f:audio=N:video=Y"
47 constexpr char kStartStreamsMessageFormat[] =
48 "START_CAST_REMOTING_STREAMS:session=%x:audio=%c:video=%c";
49 // Start acknowledgement message sent by Cast provider to CastRemotingConnector
50 // once remoting RTP streams have been set up. Examples:
51 // "STARTED_CAST_REMOTING_STREAMS:session=1f:audio_stream_id=2e:"
52 // "video_stream_id=3d"
53 // "STARTED_CAST_REMOTING_STREAMS:session=1f:video_stream_id=b33f"
54 constexpr char kStartedStreamsMessageFormatPartial[] =
55 "STARTED_CAST_REMOTING_STREAMS:session=%x";
56 constexpr char kStartedStreamsMessageAudioIdSpecifier[] = ":audio_stream_id=";
57 constexpr char kStartedStreamsMessageVideoIdSpecifier[] = ":video_stream_id=";
58 // Stop message sent by CastRemotingConnector to Cast provider. Example:
59 // "STOP_CAST_REMOTING:session=1f"
60 constexpr char kStopRemotingMessageFormat[] =
61 "STOP_CAST_REMOTING:session=%x";
62 // Stop acknowledgement message sent by Cast provider to CastRemotingConnector
63 // once remoting is available again after the last session ended. Example:
64 // "STOPPED_CAST_REMOTING:session=1f"
65 constexpr char kStoppedMessageFormat[] =
66 "STOPPED_CAST_REMOTING:session=%x";
67 // Failure message sent by Cast provider to CastRemotingConnector any time there
68 // was a fatal error (e.g., the Cast provider failed to set up the RTP streams,
69 // or there was some unexpected external event). Example:
70 // "FAILED_CAST_REMOTING:session=1f"
71 constexpr char kFailedMessageFormat[] = "FAILED_CAST_REMOTING:session=%x";
72
73 // Returns true if the given |message| matches the given |format| and the
74 // session ID in the |message| is equal to the |expected_session_id|.
75 bool IsMessageForSession(const std::string& message, const char* format,
76 unsigned int expected_session_id) {
77 unsigned int session_id;
78 if (sscanf(message.c_str(), format, &session_id) == 1)
79 return session_id == expected_session_id;
80 return false;
81 }
82
83 // Scans |message| for |specifier| and extracts the remoting stream ID that
84 // follows the specifier. Returns a negative value on error.
85 int32_t GetStreamIdFromStartedMessage(const std::string& message,
86 const char* specifier,
87 size_t specifier_length) {
88 auto start = message.find(specifier, 0, specifier_length);
89 if (start == std::string::npos)
90 return -1;
91 start += specifier_length;
92 if (start + 1 >= message.size())
93 return -1; // Must be at least one hex digit following the specifier.
94 int parsed_value;
95 if (!base::HexStringToInt(
96 message.substr(start, message.find(kMessageFieldSeparator, start)),
97 &parsed_value) ||
98 parsed_value < 0 ||
99 parsed_value > std::numeric_limits<int32_t>::max()) {
100 return -1; // Non-hex digits, or outside valid range.
101 }
102 return static_cast<int32_t>(parsed_value);
103 }
104
105 } // namespace
106
107 class CastRemotingConnector::FrameRemoterFactory
108 : public media::mojom::RemoterFactory {
109 public:
110 // |render_frame_host| represents the source render frame. Strongly binds
111 // |this| to the given Mojo interface |request|.
112 FrameRemoterFactory(content::RenderFrameHost* render_frame_host,
113 media::mojom::RemoterFactoryRequest request)
114 : host_(render_frame_host), binding_(this, std::move(request)) {
115 DCHECK(host_);
116 }
117
118 void Create(media::mojom::RemotingSourcePtr source,
119 media::mojom::RemoterRequest request) final {
120 CastRemotingConnector::Get(content::WebContents::FromRenderFrameHost(host_))
121 ->CreateBridge(std::move(source), std::move(request));
122 }
123
124 private:
125 content::RenderFrameHost* const host_;
126 const mojo::StrongBinding<media::mojom::RemoterFactory> binding_;
127
128 DISALLOW_COPY_AND_ASSIGN(FrameRemoterFactory);
129 };
130
131 class CastRemotingConnector::RemotingBridge : public media::mojom::Remoter {
132 public:
133 // Constructs a "bridge" to delegate calls between the given |source| and
134 // |connector|. Strongly binds |this| to the given Mojo interface
135 // |request|. |connector| must outlive this instance.
136 RemotingBridge(media::mojom::RemotingSourcePtr source,
137 media::mojom::RemoterRequest request,
138 CastRemotingConnector* connector)
139 : source_(std::move(source)), binding_(this, std::move(request)),
140 connector_(connector) {
141 DCHECK(connector_);
142 source_.set_connection_error_handler(base::Bind(
143 &mojo::StrongBinding<media::mojom::Remoter>::OnConnectionError,
144 base::Unretained(&binding_)));
145 connector_->RegisterBridge(this);
146 }
147
148 ~RemotingBridge() final {
149 connector_->DeregisterBridge(this, RemotingStopReason::SOURCE_GONE);
150 }
151
152 // The CastRemotingConnector calls these to call back to the RemotingSource.
153 void OnSinkAvailable() { source_->OnSinkAvailable(); }
154 void OnSinkGone() { source_->OnSinkGone(); }
155 void OnStarted() { source_->OnStarted(); }
156 void OnStartFailed(RemotingStartFailReason reason) {
157 source_->OnStartFailed(reason);
158 }
159 void OnMessageFromSink(const std::vector<uint8_t>& message) {
160 source_->OnMessageFromSink(message);
161 }
162 void OnStopped(RemotingStopReason reason) { source_->OnStopped(reason); }
163
164 // media::mojom::Remoter implementation. The source calls these to start/stop
165 // media remoting and send messages to the sink. These simply delegate to the
166 // CastRemotingConnector, which mediates to establish only one remoting
167 // session among possibly multiple requests. The connector will respond to
168 // this request by calling one of: OnStarted() or OnStartFailed().
169 void Start() final {
170 connector_->StartRemoting(this);
171 }
172 void StartDataStreams(
173 mojo::ScopedDataPipeConsumerHandle audio_pipe,
174 mojo::ScopedDataPipeConsumerHandle video_pipe,
175 media::mojom::RemotingDataStreamSenderRequest audio_sender_request,
176 media::mojom::RemotingDataStreamSenderRequest video_sender_request)
177 final {
178 connector_->StartRemotingDataStreams(
179 this, std::move(audio_pipe), std::move(video_pipe),
180 std::move(audio_sender_request), std::move(video_sender_request));
181 }
182 void Stop(RemotingStopReason reason) final {
183 connector_->StopRemoting(this, reason);
184 }
185 void SendMessageToSink(const std::vector<uint8_t>& message) final {
186 connector_->SendMessageToSink(this, message);
187 }
188
189 private:
190 media::mojom::RemotingSourcePtr source_;
191 mojo::StrongBinding<media::mojom::Remoter> binding_;
192 CastRemotingConnector* const connector_;
193
194 DISALLOW_COPY_AND_ASSIGN(RemotingBridge);
195 };
196
197 class CastRemotingConnector::MessageObserver
198 : public media_router::RouteMessageObserver {
199 public:
200 MessageObserver(media_router::MediaRouter* router,
201 const media_router::MediaRoute::Id& route_id,
202 CastRemotingConnector* connector)
203 : RouteMessageObserver(router, route_id), connector_(connector) {}
204 ~MessageObserver() final {}
205
206 private:
207 void OnMessagesReceived(
208 const std::vector<media_router::RouteMessage>& messages) final {
209 connector_->ProcessMessagesFromRoute(messages);
210 }
211
212 CastRemotingConnector* const connector_;
213 };
214
215 // static
216 CastRemotingConnector* CastRemotingConnector::Get(
217 content::WebContents* contents) {
218 CastRemotingConnector* connector = FromWebContents(contents);
219 if (connector)
220 return connector;
221 connector = new CastRemotingConnector(contents);
222 // The following transfers ownership of |connector| to WebContents.
223 contents->SetUserData(UserDataKey(), connector);
224 return connector;
225 }
226
227 // static
228 void CastRemotingConnector::CreateRemoterFactory(
229 content::RenderFrameHost* render_frame_host,
230 media::mojom::RemoterFactoryRequest request) {
231 // The new FrameRemoterFactory instance becomes owned by the message pipe
232 // associated with |request|.
233 new FrameRemoterFactory(render_frame_host, std::move(request));
234 }
235
236 CastRemotingConnector::CastRemotingConnector(content::WebContents* contents)
237 : CastRemotingConnector(
238 media_router::MediaRouterFactory::GetApiForBrowserContext(
239 contents->GetBrowserContext()),
240 media_router::MediaSourceForTabContentRemoting(contents).id()) {}
241
242 CastRemotingConnector::CastRemotingConnector(
243 media_router::MediaRouter* router,
244 const media_router::MediaSource::Id& route_source_id)
245 : media_router::MediaRoutesObserver(router, route_source_id),
246 session_counter_(0),
247 active_bridge_(nullptr),
248 weak_factory_(this) {}
249
250 CastRemotingConnector::~CastRemotingConnector() {
251 // Remoting should not be active at this point, and this instance is expected
252 // to outlive all bridges. See comment in CreateBridge().
253 DCHECK(!active_bridge_);
254 DCHECK(bridges_.empty());
255 }
256
257 void CastRemotingConnector::CreateBridge(media::mojom::RemotingSourcePtr source,
258 media::mojom::RemoterRequest request) {
259 // Create a new RemotingBridge, which will become owned by the message pipe
260 // associated with |request|. |this| CastRemotingConnector should be valid
261 // for the full lifetime of the bridge because it can be deduced that the
262 // connector will always outlive the mojo message pipe: A single WebContents
263 // will destroy the render frame tree (which destroys all associated mojo
264 // message pipes) before CastRemotingConnector. To ensure this assumption is
265 // not broken by future design changes in external modules, a DCHECK() has
266 // been placed in the CastRemotingConnector destructor as a sanity-check.
267 new RemotingBridge(std::move(source), std::move(request), this);
268 }
269
270 void CastRemotingConnector::RegisterBridge(RemotingBridge* bridge) {
271 DCHECK_CURRENTLY_ON(BrowserThread::UI);
272 DCHECK(bridges_.find(bridge) == bridges_.end());
273
274 bridges_.insert(bridge);
275 if (message_observer_ && !active_bridge_)
276 bridge->OnSinkAvailable();
277 }
278
279 void CastRemotingConnector::DeregisterBridge(RemotingBridge* bridge,
280 RemotingStopReason reason) {
281 DCHECK_CURRENTLY_ON(BrowserThread::UI);
282 DCHECK(bridges_.find(bridge) != bridges_.end());
283
284 if (bridge == active_bridge_)
285 StopRemoting(bridge, reason);
286 bridges_.erase(bridge);
287 }
288
289 void CastRemotingConnector::StartRemoting(RemotingBridge* bridge) {
290 DCHECK_CURRENTLY_ON(BrowserThread::UI);
291 DCHECK(bridges_.find(bridge) != bridges_.end());
292
293 // Refuse to start if there is no remoting route available, or if remoting is
294 // already active.
295 if (!message_observer_) {
296 bridge->OnStartFailed(RemotingStartFailReason::ROUTE_TERMINATED);
297 return;
298 }
299 if (active_bridge_) {
300 bridge->OnStartFailed(RemotingStartFailReason::CANNOT_START_MULTIPLE);
301 return;
302 }
303
304 // Notify all other sources that the sink is no longer available for remoting.
305 // A race condition is possible, where one of the other sources will try to
306 // start remoting before receiving this notification; but that attempt will
307 // just fail later on.
308 for (RemotingBridge* notifyee : bridges_) {
309 if (notifyee == bridge)
310 continue;
311 notifyee->OnSinkGone();
312 }
313
314 active_bridge_ = bridge;
315
316 // Send a start message to the Cast Provider.
317 ++session_counter_; // New remoting session ID.
318 SendMessageToProvider(
319 base::StringPrintf(kStartRemotingMessageFormat, session_counter_));
320
321 bridge->OnStarted();
322 }
323
324 void CastRemotingConnector::StartRemotingDataStreams(
325 RemotingBridge* bridge,
326 mojo::ScopedDataPipeConsumerHandle audio_pipe,
327 mojo::ScopedDataPipeConsumerHandle video_pipe,
328 media::mojom::RemotingDataStreamSenderRequest audio_sender_request,
329 media::mojom::RemotingDataStreamSenderRequest video_sender_request) {
330 DCHECK_CURRENTLY_ON(BrowserThread::UI);
331
332 // Refuse to start if there is no remoting route available, or if remoting is
333 // not active for this |bridge|.
334 if (!message_observer_ || active_bridge_ != bridge)
335 return;
336 // Also, if neither audio nor video pipe was provided, or if a request for a
337 // RemotingDataStreamSender was not provided for a data pipe, error-out early.
338 if ((!audio_pipe.is_valid() && !video_pipe.is_valid()) ||
339 (audio_pipe.is_valid() && !audio_sender_request.is_pending()) ||
340 (video_pipe.is_valid() && !video_sender_request.is_pending())) {
341 StopRemoting(active_bridge_, RemotingStopReason::DATA_SEND_FAILED);
342 return;
343 }
344
345 // Hold on to the data pipe handles and interface requests until one/both
346 // CastRemotingSenders are created and ready for use.
347 pending_audio_pipe_ = std::move(audio_pipe);
348 pending_video_pipe_ = std::move(video_pipe);
349 pending_audio_sender_request_ = std::move(audio_sender_request);
350 pending_video_sender_request_ = std::move(video_sender_request);
351
352 // Send a "start streams" message to the Cast Provider. The provider is
353 // responsible for creating and setting up a remoting Cast Streaming session
354 // that will result in new CastRemotingSender instances being created here in
355 // the browser process.
356 SendMessageToProvider(base::StringPrintf(
357 kStartStreamsMessageFormat, session_counter_,
358 pending_audio_sender_request_.is_pending() ? 'Y' : 'N',
359 pending_video_sender_request_.is_pending() ? 'Y' : 'N'));
360 }
361
362 void CastRemotingConnector::StopRemoting(RemotingBridge* bridge,
363 RemotingStopReason reason) {
364 DCHECK_CURRENTLY_ON(BrowserThread::UI);
365
366 if (active_bridge_ != bridge)
367 return;
368
369 active_bridge_ = nullptr;
370
371 // Explicitly close the data pipes (and related requests) just in case the
372 // "start streams" operation was interrupted.
373 pending_audio_pipe_.reset();
374 pending_video_pipe_.reset();
375 pending_audio_sender_request_.PassMessagePipe().reset();
376 pending_video_sender_request_.PassMessagePipe().reset();
377
378 // Cancel all outstanding callbacks related to the remoting session.
379 weak_factory_.InvalidateWeakPtrs();
380
381 // Prevent the source from trying to start again until the Cast Provider has
382 // indicated the stop operation has completed.
383 bridge->OnSinkGone();
384 // Note: At this point, all sources should think the sink is gone.
385
386 SendMessageToProvider(
387 base::StringPrintf(kStopRemotingMessageFormat, session_counter_));
388 // Note: Once the Cast Provider sends back an acknowledgement message, all
389 // sources will be notified that the remoting sink is available again.
390
391 bridge->OnStopped(reason);
392 }
393
394 void CastRemotingConnector::SendMessageToSink(
395 RemotingBridge* bridge, const std::vector<uint8_t>& message) {
396 DCHECK_CURRENTLY_ON(BrowserThread::UI);
397
398 // During an active remoting session, simply pass all binary messages through
399 // to the sink.
400 if (!message_observer_ || active_bridge_ != bridge)
401 return;
402 media_router::MediaRoutesObserver::router()->SendRouteBinaryMessage(
403 message_observer_->route_id(),
404 base::MakeUnique<std::vector<uint8_t>>(message),
405 base::Bind(&CastRemotingConnector::HandleSendMessageResult,
406 weak_factory_.GetWeakPtr()));
407 }
408
409 void CastRemotingConnector::SendMessageToProvider(const std::string& message) {
410 DCHECK_CURRENTLY_ON(BrowserThread::UI);
411
412 if (!message_observer_)
413 return;
414
415 if (active_bridge_) {
416 media_router::MediaRoutesObserver::router()->SendRouteMessage(
417 message_observer_->route_id(), message,
418 base::Bind(&CastRemotingConnector::HandleSendMessageResult,
419 weak_factory_.GetWeakPtr()));
420 } else {
421 struct Helper {
422 static void IgnoreSendMessageResult(bool ignored) {}
423 };
424 media_router::MediaRoutesObserver::router()->SendRouteMessage(
425 message_observer_->route_id(), message,
426 base::Bind(&Helper::IgnoreSendMessageResult));
427 }
428 }
429
430 void CastRemotingConnector::ProcessMessagesFromRoute(
431 const std::vector<media_router::RouteMessage>& messages) {
432 DCHECK_CURRENTLY_ON(BrowserThread::UI);
433
434 for (const media_router::RouteMessage& message : messages) {
435 switch (message.type) {
436 case media_router::RouteMessage::TEXT: // This is a control message.
apacible 2016/09/12 20:57:59 What does "control message" mean?
miu 2016/09/14 23:55:30 This is explained in the header comments for this
437 DCHECK(message.text);
438
439 // If this is a "start streams" acknowledgement message, the
440 // CastRemotingSenders should now be available to begin consuming from
441 // the data pipes.
442 if (active_bridge_ &&
443 IsMessageForSession(*message.text,
444 kStartedStreamsMessageFormatPartial,
445 session_counter_)) {
446 if (pending_audio_sender_request_.is_pending()) {
447 CastRemotingSender::FindAndBind(
448 GetStreamIdFromStartedMessage(
449 *message.text, kStartedStreamsMessageAudioIdSpecifier,
450 sizeof(kStartedStreamsMessageAudioIdSpecifier) - 1),
451 std::move(pending_audio_pipe_),
452 std::move(pending_audio_sender_request_),
453 base::Bind(&CastRemotingConnector::OnDataSendFailed,
454 weak_factory_.GetWeakPtr()));
455 }
456 if (pending_video_sender_request_.is_pending()) {
457 CastRemotingSender::FindAndBind(
458 GetStreamIdFromStartedMessage(
459 *message.text, kStartedStreamsMessageVideoIdSpecifier,
460 sizeof(kStartedStreamsMessageVideoIdSpecifier) - 1),
461 std::move(pending_video_pipe_),
462 std::move(pending_video_sender_request_),
463 base::Bind(&CastRemotingConnector::OnDataSendFailed,
464 weak_factory_.GetWeakPtr()));
465 }
466 break;
467 }
468
469 // If this is a failure message, call StopRemoting().
470 if (active_bridge_ &&
471 IsMessageForSession(*message.text, kFailedMessageFormat,
472 session_counter_)) {
473 StopRemoting(active_bridge_, RemotingStopReason::UNEXPECTED_FAILURE);
474 break;
475 }
476
477 // If this is a stop acknowledgement message, indicating that the last
478 // session was stopped, notify all sources that the sink is once again
479 // available.
480 if (IsMessageForSession(*message.text, kStoppedMessageFormat,
481 session_counter_)) {
482 if (active_bridge_) {
483 // Hmm...The Cast Provider was in a state that disagrees with this
484 // connector. Attempt to resolve this by shutting everything down to
485 // effectively reset to a known state.
486 LOG(WARNING) << "BUG: Cast Provider sent 'stopped' message during "
487 "an active remoting session.";
488 StopRemoting(active_bridge_,
489 RemotingStopReason::UNEXPECTED_FAILURE);
490 }
491 for (RemotingBridge* notifyee : bridges_)
492 notifyee->OnSinkAvailable();
493 break;
494 }
495
496 LOG(WARNING) << "BUG: Unexpected message from Cast Provider: "
497 << *message.text;
498 break;
499
500 case media_router::RouteMessage::BINARY: // This is for the source.
501 DCHECK(message.binary);
502
503 // All binary messages are passed through to the source during an active
504 // remoting session.
505 if (active_bridge_)
506 active_bridge_->OnMessageFromSink(*message.binary);
507 break;
508 }
509 }
510 }
511
512 void CastRemotingConnector::HandleSendMessageResult(bool success) {
513 DCHECK_CURRENTLY_ON(BrowserThread::UI);
514 // A single message send failure is treated as fatal to an active remoting
515 // session.
516 if (!success && active_bridge_)
517 StopRemoting(active_bridge_, RemotingStopReason::MESSAGE_SEND_FAILED);
518 }
519
520 void CastRemotingConnector::OnDataSendFailed() {
521 DCHECK_CURRENTLY_ON(BrowserThread::UI);
522 // A single data send failure is treated as fatal to an active remoting
523 // session.
524 if (active_bridge_)
525 StopRemoting(active_bridge_, RemotingStopReason::DATA_SEND_FAILED);
526 }
527
528 void CastRemotingConnector::OnRoutesUpdated(
529 const std::vector<media_router::MediaRoute>& routes,
530 const std::vector<media_router::MediaRoute::Id>& joinable_route_ids) {
531 DCHECK_CURRENTLY_ON(BrowserThread::UI);
532
533 // If a remoting route has already been identified, check that it still
534 // exists. Otherwise, shut down messaging and any active remoting, and notify
535 // the sources that remoting is no longer available.
536 if (message_observer_) {
537 for (const media_router::MediaRoute& route : routes) {
538 if (message_observer_->route_id() == route.media_route_id())
539 return; // Remoting route still exists. Take no further action.
540 }
541 message_observer_.reset();
542 if (active_bridge_)
543 StopRemoting(active_bridge_, RemotingStopReason::ROUTE_TERMINATED);
544 for (RemotingBridge* notifyee : bridges_)
545 notifyee->OnSinkGone();
546 }
547
548 // There shouldn't be an active RemotingBridge at this point, since there is
549 // currently no known remoting route.
550 DCHECK(!active_bridge_);
551
552 // Scan |routes| for a new remoting route. If one is found, begin processing
553 // messages on the route, and notify the sources that remoting is now
554 // available.
555 if (!routes.empty()) {
556 const media_router::MediaRoute& route = routes.front();
557 message_observer_.reset(new MessageObserver(
558 media_router::MediaRoutesObserver::router(), route.media_route_id(),
559 this));
560 // TODO(miu): In the future, scan the route ID for sink capabilities
561 // properties and pass these to the source in the OnSinkAvailable()
562 // notification.
563 for (RemotingBridge* notifyee : bridges_)
564 notifyee->OnSinkAvailable();
565 }
566 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698