• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright (c) 2013 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 #include "content/renderer/media/peer_connection_tracker.h"
5 
6 #include "base/strings/string_number_conversions.h"
7 #include "base/strings/utf_string_conversions.h"
8 #include "content/common/media/peer_connection_tracker_messages.h"
9 #include "content/renderer/media/rtc_media_constraints.h"
10 #include "content/renderer/media/rtc_peer_connection_handler.h"
11 #include "content/renderer/render_thread_impl.h"
12 #include "third_party/WebKit/public/platform/WebMediaConstraints.h"
13 #include "third_party/WebKit/public/platform/WebMediaStream.h"
14 #include "third_party/WebKit/public/platform/WebMediaStreamSource.h"
15 #include "third_party/WebKit/public/platform/WebMediaStreamTrack.h"
16 #include "third_party/WebKit/public/platform/WebRTCICECandidate.h"
17 #include "third_party/WebKit/public/platform/WebRTCPeerConnectionHandlerClient.h"
18 #include "third_party/WebKit/public/web/WebDocument.h"
19 #include "third_party/WebKit/public/web/WebFrame.h"
20 #include "third_party/WebKit/public/web/WebUserMediaRequest.h"
21 
22 using std::string;
23 using webrtc::MediaConstraintsInterface;
24 using blink::WebRTCPeerConnectionHandlerClient;
25 
26 namespace content {
27 
SerializeServers(const std::vector<webrtc::PeerConnectionInterface::IceServer> & servers)28 static string SerializeServers(
29     const std::vector<webrtc::PeerConnectionInterface::IceServer>& servers) {
30   string result = "[";
31   for (size_t i = 0; i < servers.size(); ++i) {
32     result += servers[i].uri;
33     if (i != servers.size() - 1)
34       result += ", ";
35   }
36   result += "]";
37   return result;
38 }
39 
GetNativeMediaConstraints(const blink::WebMediaConstraints & constraints)40 static RTCMediaConstraints GetNativeMediaConstraints(
41     const blink::WebMediaConstraints& constraints) {
42   RTCMediaConstraints native_constraints;
43 
44   if (constraints.isNull())
45     return native_constraints;
46 
47   blink::WebVector<blink::WebMediaConstraint> mandatory;
48   constraints.getMandatoryConstraints(mandatory);
49   for (size_t i = 0; i < mandatory.size(); ++i) {
50     native_constraints.AddMandatory(
51         mandatory[i].m_name.utf8(), mandatory[i].m_value.utf8(), false);
52   }
53 
54   blink::WebVector<blink::WebMediaConstraint> optional;
55   constraints.getOptionalConstraints(optional);
56   for (size_t i = 0; i < optional.size(); ++i) {
57     native_constraints.AddOptional(
58         optional[i].m_name.utf8(), optional[i].m_value.utf8(), false);
59   }
60   return native_constraints;
61 }
62 
SerializeMediaConstraints(const RTCMediaConstraints & constraints)63 static string SerializeMediaConstraints(
64     const RTCMediaConstraints& constraints) {
65   string result;
66   MediaConstraintsInterface::Constraints mandatory = constraints.GetMandatory();
67   if (!mandatory.empty()) {
68     result += "mandatory: {";
69     for (size_t i = 0; i < mandatory.size(); ++i) {
70       result += mandatory[i].key + ":" + mandatory[i].value;
71       if (i != mandatory.size() - 1)
72         result += ", ";
73     }
74     result += "}";
75   }
76   MediaConstraintsInterface::Constraints optional = constraints.GetOptional();
77   if (!optional.empty()) {
78     if (!result.empty())
79       result += ", ";
80     result += "optional: {";
81     for (size_t i = 0; i < optional.size(); ++i) {
82       result += optional[i].key + ":" + optional[i].value;
83       if (i != optional.size() - 1)
84         result += ", ";
85     }
86     result += "}";
87   }
88   return result;
89 }
90 
SerializeMediaStreamComponent(const blink::WebMediaStreamTrack component)91 static string SerializeMediaStreamComponent(
92     const blink::WebMediaStreamTrack component) {
93   string id = base::UTF16ToUTF8(component.source().id());
94   return id;
95 }
96 
SerializeMediaDescriptor(const blink::WebMediaStream & stream)97 static string SerializeMediaDescriptor(
98     const blink::WebMediaStream& stream) {
99   string label = base::UTF16ToUTF8(stream.id());
100   string result = "label: " + label;
101   blink::WebVector<blink::WebMediaStreamTrack> tracks;
102   stream.audioTracks(tracks);
103   if (!tracks.isEmpty()) {
104     result += ", audio: [";
105     for (size_t i = 0; i < tracks.size(); ++i) {
106       result += SerializeMediaStreamComponent(tracks[i]);
107       if (i != tracks.size() - 1)
108         result += ", ";
109     }
110     result += "]";
111   }
112   stream.videoTracks(tracks);
113   if (!tracks.isEmpty()) {
114     result += ", video: [";
115     for (size_t i = 0; i < tracks.size(); ++i) {
116       result += SerializeMediaStreamComponent(tracks[i]);
117       if (i != tracks.size() - 1)
118         result += ", ";
119     }
120     result += "]";
121   }
122   return result;
123 }
124 
SerializeIceTransportType(webrtc::PeerConnectionInterface::IceTransportsType type)125 static std::string SerializeIceTransportType(
126     webrtc::PeerConnectionInterface::IceTransportsType type) {
127   string transport_type;
128   switch (type) {
129   case webrtc::PeerConnectionInterface::kNone:
130     transport_type = "none";
131     break;
132   case webrtc::PeerConnectionInterface::kRelay:
133     transport_type = "relay";
134     break;
135   case webrtc::PeerConnectionInterface::kAll:
136     transport_type = "all";
137     break;
138   case webrtc::PeerConnectionInterface::kNoHost:
139     transport_type = "noHost";
140     break;
141   default:
142     NOTREACHED();
143   };
144   return transport_type;
145 }
146 
147 #define GET_STRING_OF_STATE(state)                \
148   case WebRTCPeerConnectionHandlerClient::state:  \
149     result = #state;                              \
150     break;
151 
GetSignalingStateString(WebRTCPeerConnectionHandlerClient::SignalingState state)152 static string GetSignalingStateString(
153     WebRTCPeerConnectionHandlerClient::SignalingState state) {
154   string result;
155   switch (state) {
156     GET_STRING_OF_STATE(SignalingStateStable)
157     GET_STRING_OF_STATE(SignalingStateHaveLocalOffer)
158     GET_STRING_OF_STATE(SignalingStateHaveRemoteOffer)
159     GET_STRING_OF_STATE(SignalingStateHaveLocalPrAnswer)
160     GET_STRING_OF_STATE(SignalingStateHaveRemotePrAnswer)
161     GET_STRING_OF_STATE(SignalingStateClosed)
162     default:
163       NOTREACHED();
164       break;
165   }
166   return result;
167 }
168 
GetIceConnectionStateString(WebRTCPeerConnectionHandlerClient::ICEConnectionState state)169 static string GetIceConnectionStateString(
170     WebRTCPeerConnectionHandlerClient::ICEConnectionState state) {
171   string result;
172   switch (state) {
173     GET_STRING_OF_STATE(ICEConnectionStateStarting)
174     GET_STRING_OF_STATE(ICEConnectionStateChecking)
175     GET_STRING_OF_STATE(ICEConnectionStateConnected)
176     GET_STRING_OF_STATE(ICEConnectionStateCompleted)
177     GET_STRING_OF_STATE(ICEConnectionStateFailed)
178     GET_STRING_OF_STATE(ICEConnectionStateDisconnected)
179     GET_STRING_OF_STATE(ICEConnectionStateClosed)
180     default:
181       NOTREACHED();
182       break;
183   }
184   return result;
185 }
186 
GetIceGatheringStateString(WebRTCPeerConnectionHandlerClient::ICEGatheringState state)187 static string GetIceGatheringStateString(
188     WebRTCPeerConnectionHandlerClient::ICEGatheringState state) {
189   string result;
190   switch (state) {
191     GET_STRING_OF_STATE(ICEGatheringStateNew)
192     GET_STRING_OF_STATE(ICEGatheringStateGathering)
193     GET_STRING_OF_STATE(ICEGatheringStateComplete)
194     default:
195       NOTREACHED();
196       break;
197   }
198   return result;
199 }
200 
201 // Builds a DictionaryValue from the StatsReport.
202 // The caller takes the ownership of the returned value.
203 // Note:
204 // The format must be consistent with what webrtc_internals.js expects.
205 // If you change it here, you must change webrtc_internals.js as well.
GetDictValueStats(const webrtc::StatsReport & report)206 static base::DictionaryValue* GetDictValueStats(
207     const webrtc::StatsReport& report) {
208   if (report.values.empty())
209     return NULL;
210 
211   base::DictionaryValue* dict = new base::DictionaryValue();
212   dict->SetDouble("timestamp", report.timestamp);
213 
214   base::ListValue* values = new base::ListValue();
215   dict->Set("values", values);
216 
217   for (size_t i = 0; i < report.values.size(); ++i) {
218     values->AppendString(report.values[i].display_name());
219     values->AppendString(report.values[i].value);
220   }
221   return dict;
222 }
223 
224 // Builds a DictionaryValue from the StatsReport.
225 // The caller takes the ownership of the returned value.
GetDictValue(const webrtc::StatsReport & report)226 static base::DictionaryValue* GetDictValue(const webrtc::StatsReport& report) {
227   scoped_ptr<base::DictionaryValue> stats, result;
228 
229   stats.reset(GetDictValueStats(report));
230   if (!stats)
231     return NULL;
232 
233   result.reset(new base::DictionaryValue());
234   // Note:
235   // The format must be consistent with what webrtc_internals.js expects.
236   // If you change it here, you must change webrtc_internals.js as well.
237   result->Set("stats", stats.release());
238   result->SetString("id", report.id);
239   result->SetString("type", report.type);
240 
241   return result.release();
242 }
243 
244 class InternalStatsObserver : public webrtc::StatsObserver {
245  public:
InternalStatsObserver(int lid)246   InternalStatsObserver(int lid)
247       : lid_(lid){}
248 
OnComplete(const std::vector<webrtc::StatsReport> & reports)249   virtual void OnComplete(
250       const std::vector<webrtc::StatsReport>& reports) OVERRIDE {
251     base::ListValue list;
252 
253     for (size_t i = 0; i < reports.size(); ++i) {
254       base::DictionaryValue* report = GetDictValue(reports[i]);
255       if (report)
256         list.Append(report);
257     }
258 
259     if (!list.empty())
260       RenderThreadImpl::current()->Send(
261           new PeerConnectionTrackerHost_AddStats(lid_, list));
262   }
263 
264  protected:
~InternalStatsObserver()265   virtual ~InternalStatsObserver() {}
266 
267  private:
268   int lid_;
269 };
270 
PeerConnectionTracker()271 PeerConnectionTracker::PeerConnectionTracker() : next_lid_(1) {
272 }
273 
~PeerConnectionTracker()274 PeerConnectionTracker::~PeerConnectionTracker() {
275 }
276 
OnControlMessageReceived(const IPC::Message & message)277 bool PeerConnectionTracker::OnControlMessageReceived(
278     const IPC::Message& message) {
279   bool handled = true;
280   IPC_BEGIN_MESSAGE_MAP(PeerConnectionTracker, message)
281     IPC_MESSAGE_HANDLER(PeerConnectionTracker_GetAllStats, OnGetAllStats)
282     IPC_MESSAGE_HANDLER(PeerConnectionTracker_OnSuspend, OnSuspend)
283     IPC_MESSAGE_UNHANDLED(handled = false)
284   IPC_END_MESSAGE_MAP()
285   return handled;
286 }
287 
OnGetAllStats()288 void PeerConnectionTracker::OnGetAllStats() {
289   for (PeerConnectionIdMap::iterator it = peer_connection_id_map_.begin();
290        it != peer_connection_id_map_.end(); ++it) {
291 
292     rtc::scoped_refptr<InternalStatsObserver> observer(
293         new rtc::RefCountedObject<InternalStatsObserver>(it->second));
294 
295     it->first->GetStats(
296         observer,
297         NULL,
298         webrtc::PeerConnectionInterface::kStatsOutputLevelDebug);
299   }
300 }
301 
OnSuspend()302 void PeerConnectionTracker::OnSuspend() {
303   for (PeerConnectionIdMap::iterator it = peer_connection_id_map_.begin();
304        it != peer_connection_id_map_.end(); ++it) {
305     it->first->CloseClientPeerConnection();
306   }
307 }
308 
RegisterPeerConnection(RTCPeerConnectionHandler * pc_handler,const webrtc::PeerConnectionInterface::RTCConfiguration & config,const RTCMediaConstraints & constraints,const blink::WebFrame * frame)309 void PeerConnectionTracker::RegisterPeerConnection(
310     RTCPeerConnectionHandler* pc_handler,
311     const webrtc::PeerConnectionInterface::RTCConfiguration& config,
312     const RTCMediaConstraints& constraints,
313     const blink::WebFrame* frame) {
314   DVLOG(1) << "PeerConnectionTracker::RegisterPeerConnection()";
315   PeerConnectionInfo info;
316 
317   info.lid = GetNextLocalID();
318   info.rtc_configuration =
319       "{ servers: " +  SerializeServers(config.servers) + ", " +
320       "iceTransportType: " + SerializeIceTransportType(config.type) + " }";
321 
322   info.constraints = SerializeMediaConstraints(constraints);
323   info.url = frame->document().url().spec();
324   RenderThreadImpl::current()->Send(
325       new PeerConnectionTrackerHost_AddPeerConnection(info));
326 
327   DCHECK(peer_connection_id_map_.find(pc_handler) ==
328          peer_connection_id_map_.end());
329   peer_connection_id_map_[pc_handler] = info.lid;
330 }
331 
UnregisterPeerConnection(RTCPeerConnectionHandler * pc_handler)332 void PeerConnectionTracker::UnregisterPeerConnection(
333     RTCPeerConnectionHandler* pc_handler) {
334   DVLOG(1) << "PeerConnectionTracker::UnregisterPeerConnection()";
335 
336   std::map<RTCPeerConnectionHandler*, int>::iterator it =
337       peer_connection_id_map_.find(pc_handler);
338 
339   if (it == peer_connection_id_map_.end()) {
340     // The PeerConnection might not have been registered if its initilization
341     // failed.
342     return;
343   }
344 
345   RenderThreadImpl::current()->Send(
346       new PeerConnectionTrackerHost_RemovePeerConnection(it->second));
347 
348   peer_connection_id_map_.erase(it);
349 }
350 
TrackCreateOffer(RTCPeerConnectionHandler * pc_handler,const RTCMediaConstraints & constraints)351 void PeerConnectionTracker::TrackCreateOffer(
352     RTCPeerConnectionHandler* pc_handler,
353     const RTCMediaConstraints& constraints) {
354   SendPeerConnectionUpdate(
355       pc_handler, "createOffer",
356       "constraints: {" + SerializeMediaConstraints(constraints) + "}");
357 }
358 
TrackCreateAnswer(RTCPeerConnectionHandler * pc_handler,const RTCMediaConstraints & constraints)359 void PeerConnectionTracker::TrackCreateAnswer(
360     RTCPeerConnectionHandler* pc_handler,
361     const RTCMediaConstraints& constraints) {
362   SendPeerConnectionUpdate(
363       pc_handler, "createAnswer",
364       "constraints: {" + SerializeMediaConstraints(constraints) + "}");
365 }
366 
TrackSetSessionDescription(RTCPeerConnectionHandler * pc_handler,const blink::WebRTCSessionDescription & desc,Source source)367 void PeerConnectionTracker::TrackSetSessionDescription(
368     RTCPeerConnectionHandler* pc_handler,
369     const blink::WebRTCSessionDescription& desc,
370     Source source) {
371   string sdp = base::UTF16ToUTF8(desc.sdp());
372   string type = base::UTF16ToUTF8(desc.type());
373 
374   string value = "type: " + type + ", sdp: " + sdp;
375   SendPeerConnectionUpdate(
376       pc_handler,
377       source == SOURCE_LOCAL ? "setLocalDescription" : "setRemoteDescription",
378       value);
379 }
380 
TrackUpdateIce(RTCPeerConnectionHandler * pc_handler,const webrtc::PeerConnectionInterface::RTCConfiguration & config,const RTCMediaConstraints & options)381 void PeerConnectionTracker::TrackUpdateIce(
382       RTCPeerConnectionHandler* pc_handler,
383       const webrtc::PeerConnectionInterface::RTCConfiguration& config,
384       const RTCMediaConstraints& options) {
385   string servers_string = "servers: " + SerializeServers(config.servers);
386 
387   string transport_type =
388       "iceTransportType: " + SerializeIceTransportType(config.type);
389 
390   string constraints =
391       "constraints: {" + SerializeMediaConstraints(options) + "}";
392 
393   SendPeerConnectionUpdate(
394       pc_handler,
395       "updateIce",
396       servers_string + ", " + transport_type + ", " + constraints);
397 }
398 
TrackAddIceCandidate(RTCPeerConnectionHandler * pc_handler,const blink::WebRTCICECandidate & candidate,Source source,bool succeeded)399 void PeerConnectionTracker::TrackAddIceCandidate(
400       RTCPeerConnectionHandler* pc_handler,
401       const blink::WebRTCICECandidate& candidate,
402       Source source,
403       bool succeeded) {
404   string value =
405       "sdpMid: " + base::UTF16ToUTF8(candidate.sdpMid()) + ", " +
406       "sdpMLineIndex: " + base::IntToString(candidate.sdpMLineIndex()) + ", " +
407       "candidate: " + base::UTF16ToUTF8(candidate.candidate());
408 
409   // OnIceCandidate always succeeds as it's a callback from the browser.
410   DCHECK(source != SOURCE_LOCAL || succeeded);
411 
412   string event =
413       (source == SOURCE_LOCAL) ? "onIceCandidate"
414                                : (succeeded ? "addIceCandidate"
415                                             : "addIceCandidateFailed");
416 
417   SendPeerConnectionUpdate(pc_handler, event, value);
418 }
419 
TrackAddStream(RTCPeerConnectionHandler * pc_handler,const blink::WebMediaStream & stream,Source source)420 void PeerConnectionTracker::TrackAddStream(
421     RTCPeerConnectionHandler* pc_handler,
422     const blink::WebMediaStream& stream,
423     Source source){
424   SendPeerConnectionUpdate(
425       pc_handler, source == SOURCE_LOCAL ? "addStream" : "onAddStream",
426       SerializeMediaDescriptor(stream));
427 }
428 
TrackRemoveStream(RTCPeerConnectionHandler * pc_handler,const blink::WebMediaStream & stream,Source source)429 void PeerConnectionTracker::TrackRemoveStream(
430     RTCPeerConnectionHandler* pc_handler,
431     const blink::WebMediaStream& stream,
432     Source source){
433   SendPeerConnectionUpdate(
434       pc_handler, source == SOURCE_LOCAL ? "removeStream" : "onRemoveStream",
435       SerializeMediaDescriptor(stream));
436 }
437 
TrackCreateDataChannel(RTCPeerConnectionHandler * pc_handler,const webrtc::DataChannelInterface * data_channel,Source source)438 void PeerConnectionTracker::TrackCreateDataChannel(
439     RTCPeerConnectionHandler* pc_handler,
440     const webrtc::DataChannelInterface* data_channel,
441     Source source) {
442   string value = "label: " + data_channel->label() +
443                  ", reliable: " + (data_channel->reliable() ? "true" : "false");
444   SendPeerConnectionUpdate(
445       pc_handler,
446       source == SOURCE_LOCAL ? "createLocalDataChannel" : "onRemoteDataChannel",
447       value);
448 }
449 
TrackStop(RTCPeerConnectionHandler * pc_handler)450 void PeerConnectionTracker::TrackStop(RTCPeerConnectionHandler* pc_handler) {
451   SendPeerConnectionUpdate(pc_handler, "stop", std::string());
452 }
453 
TrackSignalingStateChange(RTCPeerConnectionHandler * pc_handler,WebRTCPeerConnectionHandlerClient::SignalingState state)454 void PeerConnectionTracker::TrackSignalingStateChange(
455       RTCPeerConnectionHandler* pc_handler,
456       WebRTCPeerConnectionHandlerClient::SignalingState state) {
457   SendPeerConnectionUpdate(
458       pc_handler, "signalingStateChange", GetSignalingStateString(state));
459 }
460 
TrackIceConnectionStateChange(RTCPeerConnectionHandler * pc_handler,WebRTCPeerConnectionHandlerClient::ICEConnectionState state)461 void PeerConnectionTracker::TrackIceConnectionStateChange(
462       RTCPeerConnectionHandler* pc_handler,
463       WebRTCPeerConnectionHandlerClient::ICEConnectionState state) {
464   SendPeerConnectionUpdate(
465       pc_handler, "iceConnectionStateChange",
466       GetIceConnectionStateString(state));
467 }
468 
TrackIceGatheringStateChange(RTCPeerConnectionHandler * pc_handler,WebRTCPeerConnectionHandlerClient::ICEGatheringState state)469 void PeerConnectionTracker::TrackIceGatheringStateChange(
470       RTCPeerConnectionHandler* pc_handler,
471       WebRTCPeerConnectionHandlerClient::ICEGatheringState state) {
472   SendPeerConnectionUpdate(
473       pc_handler, "iceGatheringStateChange",
474       GetIceGatheringStateString(state));
475 }
476 
TrackSessionDescriptionCallback(RTCPeerConnectionHandler * pc_handler,Action action,const string & callback_type,const string & value)477 void PeerConnectionTracker::TrackSessionDescriptionCallback(
478     RTCPeerConnectionHandler* pc_handler, Action action,
479     const string& callback_type, const string& value) {
480   string update_type;
481   switch (action) {
482     case ACTION_SET_LOCAL_DESCRIPTION:
483       update_type = "setLocalDescription";
484       break;
485     case ACTION_SET_REMOTE_DESCRIPTION:
486       update_type = "setRemoteDescription";
487       break;
488     case ACTION_CREATE_OFFER:
489       update_type = "createOffer";
490       break;
491     case ACTION_CREATE_ANSWER:
492       update_type = "createAnswer";
493       break;
494     default:
495       NOTREACHED();
496       break;
497   }
498   update_type += callback_type;
499 
500   SendPeerConnectionUpdate(pc_handler, update_type, value);
501 }
502 
TrackOnRenegotiationNeeded(RTCPeerConnectionHandler * pc_handler)503 void PeerConnectionTracker::TrackOnRenegotiationNeeded(
504     RTCPeerConnectionHandler* pc_handler) {
505   SendPeerConnectionUpdate(pc_handler, "onRenegotiationNeeded", std::string());
506 }
507 
TrackCreateDTMFSender(RTCPeerConnectionHandler * pc_handler,const blink::WebMediaStreamTrack & track)508 void PeerConnectionTracker::TrackCreateDTMFSender(
509     RTCPeerConnectionHandler* pc_handler,
510     const blink::WebMediaStreamTrack& track) {
511   SendPeerConnectionUpdate(pc_handler, "createDTMFSender",
512                            base::UTF16ToUTF8(track.id()));
513 }
514 
TrackGetUserMedia(const blink::WebUserMediaRequest & user_media_request)515 void PeerConnectionTracker::TrackGetUserMedia(
516     const blink::WebUserMediaRequest& user_media_request) {
517   RTCMediaConstraints audio_constraints(
518       GetNativeMediaConstraints(user_media_request.audioConstraints()));
519   RTCMediaConstraints video_constraints(
520       GetNativeMediaConstraints(user_media_request.videoConstraints()));
521 
522   RenderThreadImpl::current()->Send(new PeerConnectionTrackerHost_GetUserMedia(
523       user_media_request.securityOrigin().toString().utf8(),
524       user_media_request.audio(),
525       user_media_request.video(),
526       SerializeMediaConstraints(audio_constraints),
527       SerializeMediaConstraints(video_constraints)));
528 }
529 
GetNextLocalID()530 int PeerConnectionTracker::GetNextLocalID() {
531   return next_lid_++;
532 }
533 
SendPeerConnectionUpdate(RTCPeerConnectionHandler * pc_handler,const std::string & type,const std::string & value)534 void PeerConnectionTracker::SendPeerConnectionUpdate(
535     RTCPeerConnectionHandler* pc_handler,
536     const std::string& type,
537     const std::string& value) {
538   if (peer_connection_id_map_.find(pc_handler) == peer_connection_id_map_.end())
539     return;
540 
541   RenderThreadImpl::current()->Send(
542       new PeerConnectionTrackerHost_UpdatePeerConnection(
543           peer_connection_id_map_[pc_handler], type, value));
544 }
545 
546 }  // namespace content
547