• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2014 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 "content/renderer/media/media_stream_video_source.h"
6 
7 #include <algorithm>
8 #include <limits>
9 #include <string>
10 
11 #include "base/debug/trace_event.h"
12 #include "base/logging.h"
13 #include "base/strings/string_number_conversions.h"
14 #include "content/child/child_process.h"
15 #include "content/renderer/media/media_stream_constraints_util.h"
16 #include "content/renderer/media/media_stream_video_track.h"
17 #include "content/renderer/media/video_track_adapter.h"
18 
19 namespace content {
20 
21 // Constraint keys. Specified by draft-alvestrand-constraints-resolution-00b
22 const char MediaStreamVideoSource::kMinAspectRatio[] = "minAspectRatio";
23 const char MediaStreamVideoSource::kMaxAspectRatio[] = "maxAspectRatio";
24 const char MediaStreamVideoSource::kMaxWidth[] = "maxWidth";
25 const char MediaStreamVideoSource::kMinWidth[] = "minWidth";
26 const char MediaStreamVideoSource::kMaxHeight[] = "maxHeight";
27 const char MediaStreamVideoSource::kMinHeight[] = "minHeight";
28 const char MediaStreamVideoSource::kMaxFrameRate[] = "maxFrameRate";
29 const char MediaStreamVideoSource::kMinFrameRate[] = "minFrameRate";
30 
31 const char* kSupportedConstraints[] = {
32   MediaStreamVideoSource::kMaxAspectRatio,
33   MediaStreamVideoSource::kMinAspectRatio,
34   MediaStreamVideoSource::kMaxWidth,
35   MediaStreamVideoSource::kMinWidth,
36   MediaStreamVideoSource::kMaxHeight,
37   MediaStreamVideoSource::kMinHeight,
38   MediaStreamVideoSource::kMaxFrameRate,
39   MediaStreamVideoSource::kMinFrameRate,
40 };
41 
42 const int MediaStreamVideoSource::kDefaultWidth = 640;
43 const int MediaStreamVideoSource::kDefaultHeight = 480;
44 const int MediaStreamVideoSource::kDefaultFrameRate = 30;
45 
46 namespace {
47 
48 // Google-specific key prefix. Constraints with this prefix are ignored if they
49 // are unknown.
50 const char kGooglePrefix[] = "goog";
51 
52 // Returns true if |constraint| has mandatory constraints.
HasMandatoryConstraints(const blink::WebMediaConstraints & constraints)53 bool HasMandatoryConstraints(const blink::WebMediaConstraints& constraints) {
54   blink::WebVector<blink::WebMediaConstraint> mandatory_constraints;
55   constraints.getMandatoryConstraints(mandatory_constraints);
56   return !mandatory_constraints.isEmpty();
57 }
58 
59 // Retrieve the desired max width and height from |constraints|. If not set,
60 // the |desired_width| and |desired_height| are set to
61 // std::numeric_limits<int>::max();
62 // If either max width or height is set as a mandatory constraint, the optional
63 // constraints are not checked.
GetDesiredMaxWidthAndHeight(const blink::WebMediaConstraints & constraints,int * desired_width,int * desired_height)64 void GetDesiredMaxWidthAndHeight(const blink::WebMediaConstraints& constraints,
65                                  int* desired_width, int* desired_height) {
66   *desired_width = std::numeric_limits<int>::max();
67   *desired_height = std::numeric_limits<int>::max();
68 
69   bool mandatory = GetMandatoryConstraintValueAsInteger(
70       constraints,
71       MediaStreamVideoSource::kMaxWidth,
72       desired_width);
73   mandatory |= GetMandatoryConstraintValueAsInteger(
74       constraints,
75       MediaStreamVideoSource::kMaxHeight,
76       desired_height);
77   if (mandatory)
78     return;
79 
80   GetOptionalConstraintValueAsInteger(constraints,
81                                       MediaStreamVideoSource::kMaxWidth,
82                                       desired_width);
83   GetOptionalConstraintValueAsInteger(constraints,
84                                       MediaStreamVideoSource::kMaxHeight,
85                                       desired_height);
86 }
87 
88 // Retrieve the desired max and min aspect ratio from |constraints|. If not set,
89 // the |min_aspect_ratio| is set to 0 and |max_aspect_ratio| is set to
90 // std::numeric_limits<double>::max();
91 // If either min or max aspect ratio is set as a mandatory constraint, the
92 // optional constraints are not checked.
GetDesiredMinAndMaxAspectRatio(const blink::WebMediaConstraints & constraints,double * min_aspect_ratio,double * max_aspect_ratio)93 void GetDesiredMinAndMaxAspectRatio(
94     const blink::WebMediaConstraints& constraints,
95     double* min_aspect_ratio,
96     double* max_aspect_ratio) {
97   *min_aspect_ratio = 0;
98   *max_aspect_ratio = std::numeric_limits<double>::max();
99 
100   bool mandatory = GetMandatoryConstraintValueAsDouble(
101       constraints,
102       MediaStreamVideoSource::kMinAspectRatio,
103       min_aspect_ratio);
104   mandatory |= GetMandatoryConstraintValueAsDouble(
105       constraints,
106       MediaStreamVideoSource::kMaxAspectRatio,
107       max_aspect_ratio);
108   if (mandatory)
109     return;
110 
111   GetOptionalConstraintValueAsDouble(
112       constraints,
113       MediaStreamVideoSource::kMinAspectRatio,
114       min_aspect_ratio);
115   GetOptionalConstraintValueAsDouble(
116       constraints,
117       MediaStreamVideoSource::kMaxAspectRatio,
118       max_aspect_ratio);
119 }
120 
121 // Returns true if |constraint| is fulfilled. |format| can be changed by a
122 // constraint, e.g. the frame rate can be changed by setting maxFrameRate.
UpdateFormatForConstraint(const blink::WebMediaConstraint & constraint,bool mandatory,media::VideoCaptureFormat * format)123 bool UpdateFormatForConstraint(
124     const blink::WebMediaConstraint& constraint,
125     bool mandatory,
126     media::VideoCaptureFormat* format) {
127   DCHECK(format != NULL);
128 
129   if (!format->IsValid())
130     return false;
131 
132   std::string constraint_name = constraint.m_name.utf8();
133   std::string constraint_value = constraint.m_value.utf8();
134 
135   if (constraint_name.find(kGooglePrefix) == 0) {
136     // These are actually options, not constraints, so they can be satisfied
137     // regardless of the format.
138     return true;
139   }
140 
141   if (constraint_name == MediaStreamSource::kSourceId) {
142     // This is a constraint that doesn't affect the format.
143     return true;
144   }
145 
146   // Ignore Chrome specific Tab capture constraints.
147   if (constraint_name == kMediaStreamSource ||
148       constraint_name == kMediaStreamSourceId)
149     return true;
150 
151   if (constraint_name == MediaStreamVideoSource::kMinAspectRatio ||
152       constraint_name == MediaStreamVideoSource::kMaxAspectRatio) {
153     // These constraints are handled by cropping if the camera outputs the wrong
154     // aspect ratio.
155     double value;
156     return base::StringToDouble(constraint_value, &value);
157   }
158 
159   double value = 0.0;
160   if (!base::StringToDouble(constraint_value, &value)) {
161     DLOG(WARNING) << "Can't parse MediaStream constraint. Name:"
162                   <<  constraint_name << " Value:" << constraint_value;
163     return false;
164   }
165 
166   if (constraint_name == MediaStreamVideoSource::kMinWidth) {
167     return (value <= format->frame_size.width());
168   } else if (constraint_name == MediaStreamVideoSource::kMaxWidth) {
169     return value > 0.0;
170   } else if (constraint_name == MediaStreamVideoSource::kMinHeight) {
171     return (value <= format->frame_size.height());
172   } else if (constraint_name == MediaStreamVideoSource::kMaxHeight) {
173      return value > 0.0;
174   } else if (constraint_name == MediaStreamVideoSource::kMinFrameRate) {
175     return (value <= format->frame_rate);
176   } else if (constraint_name == MediaStreamVideoSource::kMaxFrameRate) {
177     if (value == 0.0) {
178       // The frame rate is set by constraint.
179       // Don't allow 0 as frame rate if it is a mandatory constraint.
180       // Set the frame rate to 1 if it is not mandatory.
181       if (mandatory) {
182         return false;
183       } else {
184         value = 1.0;
185       }
186     }
187     format->frame_rate =
188         (format->frame_rate > value) ? value : format->frame_rate;
189     return true;
190   } else {
191     LOG(WARNING) << "Found unknown MediaStream constraint. Name:"
192                  <<  constraint_name << " Value:" << constraint_value;
193     return false;
194   }
195 }
196 
197 // Removes media::VideoCaptureFormats from |formats| that don't meet
198 // |constraint|.
FilterFormatsByConstraint(const blink::WebMediaConstraint & constraint,bool mandatory,media::VideoCaptureFormats * formats)199 void FilterFormatsByConstraint(
200     const blink::WebMediaConstraint& constraint,
201     bool mandatory,
202     media::VideoCaptureFormats* formats) {
203   DVLOG(3) << "FilterFormatsByConstraint("
204            << "{ constraint.m_name = " << constraint.m_name.utf8()
205            << "  constraint.m_value = " << constraint.m_value.utf8()
206            << "  mandatory =  " << mandatory << "})";
207   media::VideoCaptureFormats::iterator format_it = formats->begin();
208   while (format_it != formats->end()) {
209     // Modify the format_it to fulfill the constraint if possible.
210     // Delete it otherwise.
211     if (!UpdateFormatForConstraint(constraint, mandatory, &(*format_it))) {
212       format_it = formats->erase(format_it);
213     } else {
214       ++format_it;
215     }
216   }
217 }
218 
219 // Returns the media::VideoCaptureFormats that matches |constraints|.
FilterFormats(const blink::WebMediaConstraints & constraints,const media::VideoCaptureFormats & supported_formats)220 media::VideoCaptureFormats FilterFormats(
221     const blink::WebMediaConstraints& constraints,
222     const media::VideoCaptureFormats& supported_formats) {
223   if (constraints.isNull()) {
224     return supported_formats;
225   }
226 
227   double max_aspect_ratio;
228   double min_aspect_ratio;
229   GetDesiredMinAndMaxAspectRatio(constraints,
230                                  &min_aspect_ratio,
231                                  &max_aspect_ratio);
232 
233   if (min_aspect_ratio > max_aspect_ratio || max_aspect_ratio < 0.05f) {
234     DLOG(WARNING) << "Wrong requested aspect ratio.";
235     return media::VideoCaptureFormats();
236   }
237 
238   int min_width = 0;
239   GetMandatoryConstraintValueAsInteger(constraints,
240                                        MediaStreamVideoSource::kMinWidth,
241                                        &min_width);
242   int min_height = 0;
243   GetMandatoryConstraintValueAsInteger(constraints,
244                                        MediaStreamVideoSource::kMinHeight,
245                                        &min_height);
246   int max_width;
247   int max_height;
248   GetDesiredMaxWidthAndHeight(constraints, &max_width, &max_height);
249 
250   if (min_width > max_width || min_height > max_height)
251     return media::VideoCaptureFormats();
252 
253   blink::WebVector<blink::WebMediaConstraint> mandatory;
254   blink::WebVector<blink::WebMediaConstraint> optional;
255   constraints.getMandatoryConstraints(mandatory);
256   constraints.getOptionalConstraints(optional);
257   media::VideoCaptureFormats candidates = supported_formats;
258   for (size_t i = 0; i < mandatory.size(); ++i)
259     FilterFormatsByConstraint(mandatory[i], true, &candidates);
260 
261   if (candidates.empty())
262     return candidates;
263 
264   // Ok - all mandatory checked and we still have candidates.
265   // Let's try filtering using the optional constraints. The optional
266   // constraints must be filtered in the order they occur in |optional|.
267   // But if a constraint produce zero candidates, the constraint is ignored and
268   // the next constraint is tested.
269   // http://dev.w3.org/2011/webrtc/editor/getusermedia.html#idl-def-Constraints
270   for (size_t i = 0; i < optional.size(); ++i) {
271     media::VideoCaptureFormats current_candidates = candidates;
272     FilterFormatsByConstraint(optional[i], false, &current_candidates);
273     if (!current_candidates.empty()) {
274       candidates = current_candidates;
275     }
276   }
277 
278   // We have done as good as we can to filter the supported resolutions.
279   return candidates;
280 }
281 
GetBestFormatBasedOnArea(const media::VideoCaptureFormats & formats,int area)282 const media::VideoCaptureFormat& GetBestFormatBasedOnArea(
283     const media::VideoCaptureFormats& formats,
284     int area) {
285   media::VideoCaptureFormats::const_iterator it = formats.begin();
286   media::VideoCaptureFormats::const_iterator best_it = formats.begin();
287   int best_diff = std::numeric_limits<int>::max();
288   for (; it != formats.end(); ++it) {
289     int diff = abs(area - it->frame_size.width() * it->frame_size.height());
290     if (diff < best_diff) {
291       best_diff = diff;
292       best_it = it;
293     }
294   }
295   return *best_it;
296 }
297 
298 // Find the format that best matches the default video size.
299 // This algorithm is chosen since a resolution must be picked even if no
300 // constraints are provided. We don't just select the maximum supported
301 // resolution since higher resolutions cost more in terms of complexity and
302 // many cameras have lower frame rate and have more noise in the image at
303 // their maximum supported resolution.
GetBestCaptureFormat(const media::VideoCaptureFormats & formats,const blink::WebMediaConstraints & constraints,media::VideoCaptureFormat * capture_format)304 void GetBestCaptureFormat(
305     const media::VideoCaptureFormats& formats,
306     const blink::WebMediaConstraints& constraints,
307     media::VideoCaptureFormat* capture_format) {
308   DCHECK(!formats.empty());
309 
310   int max_width;
311   int max_height;
312   GetDesiredMaxWidthAndHeight(constraints, &max_width, &max_height);
313 
314   *capture_format = GetBestFormatBasedOnArea(
315       formats,
316       std::min(max_width, MediaStreamVideoSource::kDefaultWidth) *
317       std::min(max_height, MediaStreamVideoSource::kDefaultHeight));
318 }
319 
320 }  // anonymous namespace
321 
322 // static
GetVideoSource(const blink::WebMediaStreamSource & source)323 MediaStreamVideoSource* MediaStreamVideoSource::GetVideoSource(
324     const blink::WebMediaStreamSource& source) {
325   return static_cast<MediaStreamVideoSource*>(source.extraData());
326 }
327 
328 // static
IsConstraintSupported(const std::string & name)329 bool MediaStreamVideoSource::IsConstraintSupported(const std::string& name) {
330   for (size_t i = 0; i < arraysize(kSupportedConstraints); ++i) {
331     if (kSupportedConstraints[i] == name)
332       return true;
333   }
334   return false;
335 }
336 
MediaStreamVideoSource()337 MediaStreamVideoSource::MediaStreamVideoSource()
338     : state_(NEW),
339       track_adapter_(new VideoTrackAdapter(
340           ChildProcess::current()->io_message_loop_proxy())),
341       weak_factory_(this) {
342 }
343 
~MediaStreamVideoSource()344 MediaStreamVideoSource::~MediaStreamVideoSource() {
345   DVLOG(3) << "~MediaStreamVideoSource()";
346 }
347 
AddTrack(MediaStreamVideoTrack * track,const VideoCaptureDeliverFrameCB & frame_callback,const blink::WebMediaConstraints & constraints,const ConstraintsCallback & callback)348 void MediaStreamVideoSource::AddTrack(
349     MediaStreamVideoTrack* track,
350     const VideoCaptureDeliverFrameCB& frame_callback,
351     const blink::WebMediaConstraints& constraints,
352     const ConstraintsCallback& callback) {
353   DCHECK(CalledOnValidThread());
354   DCHECK(!constraints.isNull());
355   DCHECK(std::find(tracks_.begin(), tracks_.end(),
356                    track) == tracks_.end());
357   tracks_.push_back(track);
358 
359   requested_constraints_.push_back(
360       RequestedConstraints(track, frame_callback, constraints, callback));
361 
362   switch (state_) {
363     case NEW: {
364       // Tab capture and Screen capture needs the maximum requested height
365       // and width to decide on the resolution.
366       int max_requested_width = 0;
367       GetMandatoryConstraintValueAsInteger(constraints, kMaxWidth,
368                                            &max_requested_width);
369 
370       int max_requested_height = 0;
371       GetMandatoryConstraintValueAsInteger(constraints, kMaxHeight,
372                                            &max_requested_height);
373 
374       state_ = RETRIEVING_CAPABILITIES;
375       GetCurrentSupportedFormats(
376           max_requested_width,
377           max_requested_height,
378           base::Bind(&MediaStreamVideoSource::OnSupportedFormats,
379                      weak_factory_.GetWeakPtr()));
380 
381       break;
382     }
383     case STARTING:
384     case RETRIEVING_CAPABILITIES: {
385       // The |callback| will be triggered once the source has started or
386       // the capabilities have been retrieved.
387       break;
388     }
389     case ENDED:
390     case STARTED: {
391       // Currently, reconfiguring the source is not supported.
392       FinalizeAddTrack();
393     }
394   }
395 }
396 
RemoveTrack(MediaStreamVideoTrack * video_track)397 void MediaStreamVideoSource::RemoveTrack(MediaStreamVideoTrack* video_track) {
398   DCHECK(CalledOnValidThread());
399   std::vector<MediaStreamVideoTrack*>::iterator it =
400       std::find(tracks_.begin(), tracks_.end(), video_track);
401   DCHECK(it != tracks_.end());
402   tracks_.erase(it);
403 
404   // Check if |video_track| is waiting for applying new constraints and remove
405   // the request in that case.
406   for (std::vector<RequestedConstraints>::iterator it =
407            requested_constraints_.begin();
408        it != requested_constraints_.end(); ++it) {
409     if (it->track == video_track) {
410       requested_constraints_.erase(it);
411       break;
412     }
413   }
414   // Call |frame_adapter_->RemoveTrack| here even if adding the track has
415   // failed and |frame_adapter_->AddCallback| has not been called.
416   track_adapter_->RemoveTrack(video_track);
417 
418   if (tracks_.empty())
419     StopSource();
420 }
421 
422 const scoped_refptr<base::MessageLoopProxy>&
io_message_loop() const423 MediaStreamVideoSource::io_message_loop() const {
424   DCHECK(CalledOnValidThread());
425   return track_adapter_->io_message_loop();
426 }
427 
DoStopSource()428 void MediaStreamVideoSource::DoStopSource() {
429   DCHECK(CalledOnValidThread());
430   DVLOG(3) << "DoStopSource()";
431   if (state_ == ENDED)
432     return;
433   StopSourceImpl();
434   state_ = ENDED;
435   SetReadyState(blink::WebMediaStreamSource::ReadyStateEnded);
436 }
437 
OnSupportedFormats(const media::VideoCaptureFormats & formats)438 void MediaStreamVideoSource::OnSupportedFormats(
439     const media::VideoCaptureFormats& formats) {
440   DCHECK(CalledOnValidThread());
441   DCHECK_EQ(RETRIEVING_CAPABILITIES, state_);
442 
443   supported_formats_ = formats;
444   if (!FindBestFormatWithConstraints(supported_formats_,
445                                      &current_format_)) {
446     SetReadyState(blink::WebMediaStreamSource::ReadyStateEnded);
447     // This object can be deleted after calling FinalizeAddTrack. See comment
448     // in the header file.
449     FinalizeAddTrack();
450     return;
451   }
452 
453   state_ = STARTING;
454   DVLOG(3) << "Starting the capturer with"
455            << " width = " << current_format_.frame_size.width()
456            << " height = " << current_format_.frame_size.height()
457            << " frame rate = " << current_format_.frame_rate;
458 
459   media::VideoCaptureParams params;
460   params.requested_format = current_format_;
461   StartSourceImpl(
462       params,
463       base::Bind(&VideoTrackAdapter::DeliverFrameOnIO, track_adapter_));
464 }
465 
FindBestFormatWithConstraints(const media::VideoCaptureFormats & formats,media::VideoCaptureFormat * best_format)466 bool MediaStreamVideoSource::FindBestFormatWithConstraints(
467     const media::VideoCaptureFormats& formats,
468     media::VideoCaptureFormat* best_format) {
469   // Find the first constraints that we can fulfill.
470   for (std::vector<RequestedConstraints>::iterator request_it =
471            requested_constraints_.begin();
472        request_it != requested_constraints_.end(); ++request_it) {
473     const blink::WebMediaConstraints& requested_constraints =
474         request_it->constraints;
475 
476     // If the source doesn't support capability enumeration it is still ok if
477     // no mandatory constraints have been specified. That just means that
478     // we will start with whatever format is native to the source.
479     if (formats.empty() && !HasMandatoryConstraints(requested_constraints)) {
480       *best_format = media::VideoCaptureFormat();
481       return true;
482     }
483     media::VideoCaptureFormats filtered_formats =
484         FilterFormats(requested_constraints, formats);
485     if (filtered_formats.size() > 0) {
486       // A request with constraints that can be fulfilled.
487       GetBestCaptureFormat(filtered_formats,
488                            requested_constraints,
489                            best_format);
490       return true;
491     }
492   }
493   return false;
494 }
495 
OnStartDone(bool success)496 void MediaStreamVideoSource::OnStartDone(bool success) {
497   DCHECK(CalledOnValidThread());
498   DVLOG(3) << "OnStartDone({success =" << success << "})";
499   if (success) {
500     DCHECK_EQ(STARTING, state_);
501     state_ = STARTED;
502     SetReadyState(blink::WebMediaStreamSource::ReadyStateLive);
503   } else {
504     state_ = ENDED;
505     SetReadyState(blink::WebMediaStreamSource::ReadyStateEnded);
506     StopSourceImpl();
507   }
508 
509   // This object can be deleted after calling FinalizeAddTrack. See comment in
510   // the header file.
511   FinalizeAddTrack();
512 }
513 
FinalizeAddTrack()514 void MediaStreamVideoSource::FinalizeAddTrack() {
515   media::VideoCaptureFormats formats;
516   formats.push_back(current_format_);
517 
518   std::vector<RequestedConstraints> callbacks;
519   callbacks.swap(requested_constraints_);
520   for (std::vector<RequestedConstraints>::iterator it = callbacks.begin();
521        it != callbacks.end(); ++it) {
522     // The track has been added successfully if the source has started and
523     // there are either no mandatory constraints and the source doesn't expose
524     // its format capabilities, or the constraints and the format match.
525     // For example, a remote source doesn't expose its format capabilities.
526     bool success =
527         state_ == STARTED &&
528         ((!current_format_.IsValid() && !HasMandatoryConstraints(
529             it->constraints)) ||
530          !FilterFormats(it->constraints, formats).empty());
531 
532     if (success) {
533       int max_width;
534       int max_height;
535       GetDesiredMaxWidthAndHeight(it->constraints, &max_width, &max_height);
536       double max_aspect_ratio;
537       double min_aspect_ratio;
538       GetDesiredMinAndMaxAspectRatio(it->constraints,
539                                      &min_aspect_ratio,
540                                      &max_aspect_ratio);
541       track_adapter_->AddTrack(it->track,it->frame_callback,
542                                max_width, max_height,
543                                min_aspect_ratio, max_aspect_ratio);
544     }
545 
546     DVLOG(3) << "FinalizeAddTrack() success " << success;
547 
548     if (!it->callback.is_null())
549       it->callback.Run(this, success);
550   }
551 }
552 
SetReadyState(blink::WebMediaStreamSource::ReadyState state)553 void MediaStreamVideoSource::SetReadyState(
554     blink::WebMediaStreamSource::ReadyState state) {
555   if (!owner().isNull()) {
556     owner().setReadyState(state);
557   }
558   for (std::vector<MediaStreamVideoTrack*>::iterator it = tracks_.begin();
559        it != tracks_.end(); ++it) {
560     (*it)->OnReadyStateChanged(state);
561   }
562 }
563 
RequestedConstraints(MediaStreamVideoTrack * track,const VideoCaptureDeliverFrameCB & frame_callback,const blink::WebMediaConstraints & constraints,const ConstraintsCallback & callback)564 MediaStreamVideoSource::RequestedConstraints::RequestedConstraints(
565     MediaStreamVideoTrack* track,
566     const VideoCaptureDeliverFrameCB& frame_callback,
567     const blink::WebMediaConstraints& constraints,
568     const ConstraintsCallback& callback)
569     : track(track),
570       frame_callback(frame_callback),
571       constraints(constraints),
572       callback(callback) {
573 }
574 
~RequestedConstraints()575 MediaStreamVideoSource::RequestedConstraints::~RequestedConstraints() {
576 }
577 
578 }  // namespace content
579