[M120 Migration][MM] Handle live stream duration and currenttime
[platform/framework/web/chromium-efl.git] / tizen_src / chromium_impl / content / browser / media / tizen_renderer_impl.cc
1 // Copyright 2022 Samsung Electronics Inc. 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 "tizen_src/chromium_impl/content/browser/media/tizen_renderer_impl.h"
6
7 #include "base/command_line.h"
8 #include "base/functional/bind.h"
9 #include "base/task/single_thread_task_runner.h"
10 #include "base/trace_event/trace_event.h"
11 #include "content/browser/web_contents/web_contents_impl.h"
12 #include "content/common/content_client_export.h"
13 #include "content/public/browser/browser_context.h"
14 #include "content/public/browser/browser_task_traits.h"
15 #include "content/public/browser/browser_thread.h"
16 #include "content/public/browser/content_browser_client.h"
17 #include "content/public/browser/render_process_host.h"
18 #include "content/public/browser/web_contents.h"
19 #include "content/public/common/content_client.h"
20 #include "media/base/audio_decoder_config.h"
21 #include "media/base/media_resource.h"
22 #include "media/base/media_switches.h"
23 #include "media/base/renderer_client.h"
24 #include "media/base/video_decoder_config.h"
25 #include "tizen_src/chromium_impl/content/browser/media/media_player_renderer_web_contents_observer.h"
26 #include "tizen_src/chromium_impl/media/filters/media_player_registry.h"
27 #include "tizen_src/chromium_impl/media/filters/media_player_tizen.h"
28
29 #if defined(TIZEN_VIDEO_HOLE)
30 #include "content/browser/renderer_host/render_widget_host_view_aura.h"
31 #endif
32
33 #if defined(TIZEN_TBM_SUPPORT)
34 namespace media {
35 void DestroyMediaPacket(void* media_packet, int player_id) {
36   MediaPlayerTizen* player =
37       MediaPlayerRegistry::GetInstance()->GetMediaPlayer(player_id);
38   if (!player) {
39     // TODO: All media packets should be removed before player is destroyed.
40     LOG(WARNING) << __func__
41                  << " player already destroyed. player_id: " << player_id;
42     return;
43   }
44   player->DestroyMediaPacket(media_packet);
45 }
46 }  // namespace media
47 #endif
48
49 namespace content {
50
51 TizenRendererImpl::TizenRendererImpl(
52     int process_id,
53     int routing_id,
54     WebContents* web_contents,
55     mojo::PendingReceiver<RendererExtension> renderer_extension_receiver,
56     mojo::PendingRemote<ClientExtension> client_extension_remote)
57     : client_extension_(std::move(client_extension_remote)),
58       render_process_id_(process_id),
59       routing_id_(routing_id),
60       volume_(kDefaultVolume),
61 #if defined(TIZEN_VIDEO_HOLE)
62       video_rect_(gfx::RectF()),
63 #endif
64 #if BUILDFLAG(IS_TIZEN_TV)
65       notify_playback_state_(media::kPlaybackStop),
66 #endif
67       renderer_extension_receiver_(this,
68                                    std::move(renderer_extension_receiver)),
69       web_contents_(web_contents) {
70   LOG(INFO) << "(" << static_cast<void*>(this) << ") " << __func__;
71   DCHECK_EQ(WebContents::FromRenderFrameHost(
72                 RenderFrameHost::FromID(process_id, routing_id)),
73             web_contents);
74
75   WebContentsImpl* web_contents_impl =
76       static_cast<WebContentsImpl*>(web_contents);
77   web_contents_muted_ = web_contents_impl && web_contents_impl->IsAudioMuted();
78   task_runner_ = base::SingleThreadTaskRunner::GetCurrentDefault();
79
80   if (web_contents) {
81     MediaPlayerRendererWebContentsObserver::CreateForWebContents(web_contents);
82     web_contents_observer_ =
83         MediaPlayerRendererWebContentsObserver::FromWebContents(web_contents);
84     if (web_contents_observer_)
85       web_contents_observer_->AddMediaPlayerRenderer(this);
86   }
87 }
88
89 TizenRendererImpl::TizenRendererImpl(
90     const scoped_refptr<base::SingleThreadTaskRunner>& task_runner,
91     media::VideoRendererSink* sink)
92     : state_(STATE_UNINITIALIZED),
93       task_runner_(task_runner),
94       sink_(sink),
95 #if defined(TIZEN_VIDEO_HOLE)
96       video_rect_(gfx::RectF()),
97 #endif
98       renderer_extension_receiver_(this) {
99   LOG(INFO) << "(" << static_cast<void*>(this) << ") " << __func__;
100   // TODO(dalecurtis): Remove once experiments for http://crbug.com/470940 are
101   // complete.
102   int threshold_ms = 0;
103   std::string threshold_ms_str(
104       base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
105           switches::kVideoUnderflowThresholdMs));
106   if (base::StringToInt(threshold_ms_str, &threshold_ms) && threshold_ms > 0) {
107     NOTIMPLEMENTED();
108   }
109 }
110
111 TizenRendererImpl::~TizenRendererImpl() {
112   LOG(INFO) << "(" << static_cast<void*>(this) << ") " << __func__;
113   DCHECK(task_runner_->BelongsToCurrentThread());
114
115   weak_factory_.InvalidateWeakPtrs();
116
117   media::MediaPlayerRegistry::GetInstance()->UnregisterMediaPlayer(player_id_);
118
119   if (media_player_) {
120     media_player_->Release();
121     media_player_->SetMediaPlayerClient(nullptr);
122   }
123
124   if (web_contents_observer_)
125     web_contents_observer_->RemoveMediaPlayerRenderer(this);
126 }
127
128 void TizenRendererImpl::Initialize(media::MediaResource* media_resource,
129                                    media::RendererClient* client,
130                                    media::PipelineStatusCallback init_cb) {
131   LOG(INFO) << "(" << static_cast<void*>(this) << ") " << __func__;
132   DCHECK(task_runner_->BelongsToCurrentThread());
133   DCHECK_EQ(state_, STATE_UNINITIALIZED);
134   DCHECK(init_cb);
135   DCHECK(client);
136   TRACE_EVENT_ASYNC_BEGIN0("media", "TizenRendererImpl::Initialize", this);
137
138   client_ = client;
139   init_cb_ = std::move(init_cb);
140   media_resource_ = media_resource;
141
142   std::string user_agent;
143   content::WebContents* web_contents = GetWebContents();
144   if (web_contents) {
145     user_agent = web_contents->GetUserAgentOverride().ua_string_override;
146     if (user_agent.empty())
147       user_agent = content::GetContentClientExport()->browser()->GetUserAgent();
148   }
149
150   // Create a media player and get player id for the created media player.
151   media_player_ = media::MediaPlayerRegistry::GetInstance()->CreateMediaPlayer(
152       media_resource_, user_agent, volume_, player_id_);
153
154   if (!media_player_->CreatePlayer(player_id_)) {
155     media::MediaPlayerRegistry::GetInstance()->UnregisterMediaPlayer(
156         player_id_);
157     std::move(init_cb_).Run(media::PIPELINE_ERROR_INITIALIZATION_FAILED);
158     return;
159   }
160
161   if (media_resource_->GetType() != media::MediaResource::Type::KUrl) {
162     audio_stream_ =
163         media_resource_->GetFirstStream(media::DemuxerStream::AUDIO);
164     if (audio_stream_) {
165       media_player_->SetMediaType(media::DemuxerStream::AUDIO);
166     }
167     video_stream_ =
168         media_resource_->GetFirstStream(media::DemuxerStream::VIDEO);
169     if (video_stream_) {
170       media_player_->SetMediaType(media::DemuxerStream::VIDEO);
171     }
172   }
173
174 #if defined(TIZEN_VIDEO_HOLE)
175   if (web_contents) {
176     if (RenderWidgetHostViewAura* view_aura =
177             static_cast<RenderWidgetHostViewAura*>(
178                 web_contents->GetRenderWidgetHostView())) {
179       view_aura->SetWebViewMovedCallback(base::BindRepeating(
180           &TizenRendererImpl::OnWebViewMoved, base::Unretained(this)));
181       LOG(INFO) << "SetPositionMovedCallbacks called!";
182     }
183   }
184 #endif
185
186   // TODO: return unsupported error for CDM.
187   std::move(init_cb_).Run(media::PIPELINE_OK);
188
189   media_player_->SetMediaPlayerClient(this);
190   media_player_->SetTaskRunner(task_runner_.get());
191
192   if (!media::MediaPlayerRegistry::GetInstance()->ActivateMediaPlayer(
193           player_id_, !resource_conflicted_)) {
194     LOG(INFO) << "(" << static_cast<void*>(this) << ") " << __func__
195               << " Can not initialize the player id: " << player_id_;
196     return;
197   }
198
199   is_suspended_ = false;
200   resource_conflicted_ = false;
201
202   // Initialize and Prepare early when resource type is URL.
203   if (media_resource_->GetType() == media::MediaResource::Type::KUrl) {
204     SetPlayerInitialize();
205     SetPlayerPrepare();
206   }
207 }
208
209 void TizenRendererImpl::SetStreamInfo() {
210   if (media_resource_->GetType() == media::MediaResource::Type::KUrl) {
211     media_player_->SetStreamInfo(media::DemuxerStream::VIDEO, 0);
212     return;
213   }
214
215   if (audio_stream_)
216     media_player_->SetStreamInfo(media::DemuxerStream::AUDIO, audio_stream_);
217   if (video_stream_)
218     media_player_->SetStreamInfo(media::DemuxerStream::VIDEO, video_stream_);
219 }
220
221 void TizenRendererImpl::SetPlayerInitialize() {
222   if (media_player_->IsInitialized()) {
223     LOG(INFO) << __func__ << " Already initialized.";
224     return;
225   }
226
227 #if defined(TIZEN_VIDEO_HOLE)
228   if (is_video_hole_) {
229     SetPlayerVideoHole();
230     SetPlayerMediaGeometry();
231   }
232 #endif
233
234   media_player_->Initialize(sink_);
235 }
236
237 void TizenRendererImpl::SetPlayerPrepare() {
238   if (!media_player_->CanPrepare()) {
239     LOG(INFO) << __func__ << " Already preparing or prepared.";
240     return;
241   }
242
243   SetStreamInfo();
244   SetPlayerVolume();
245
246 #if defined(TIZEN_VIDEO_HOLE)
247   media_player_->PrepareVideoHole();
248 #endif
249
250   media_player_->Prepare();
251 }
252
253 void TizenRendererImpl::SetCdm(media::CdmContext* cdm_context,
254                                CdmAttachedCB cdm_attached_cb) {
255   DCHECK(task_runner_->BelongsToCurrentThread());
256   DCHECK(cdm_context);
257   TRACE_EVENT0("media", "TizenRendererImpl::SetCdm");
258   NOTIMPLEMENTED();
259 }
260
261 void TizenRendererImpl::SetLatencyHint(
262     absl::optional<base::TimeDelta> latency_hint) {
263   DCHECK(!latency_hint || (*latency_hint >= base::TimeDelta()));
264   DCHECK(task_runner_->BelongsToCurrentThread());
265   NOTIMPLEMENTED();
266 }
267
268 void TizenRendererImpl::SetPreservesPitch(bool preserves_pitch) {
269   DCHECK(task_runner_->BelongsToCurrentThread());
270   NOTIMPLEMENTED();
271 }
272
273 void TizenRendererImpl::Flush(base::OnceClosure flush_cb) {
274   DCHECK(task_runner_->BelongsToCurrentThread());
275   TRACE_EVENT_ASYNC_BEGIN0("media", "TizenRendererImpl::Flush", this);
276
277   if (state_ != STATE_PLAYING) {
278     DCHECK_EQ(state_, STATE_ERROR);
279     return;
280   }
281
282   media_player_->Flush(std::move(flush_cb));
283 }
284
285 void TizenRendererImpl::Seek(base::TimeDelta time, base::OnceClosure seek_cb) {
286   LOG(INFO) << "(" << static_cast<void*>(this) << ") " << __func__
287             << " time : " << time.InMicroseconds();
288   media_player_->Seek(time, std::move(seek_cb));
289 }
290
291 #if defined(TIZEN_VIDEO_HOLE)
292 void TizenRendererImpl::SetVideoHole(bool is_video_hole) {
293   if (is_video_hole_ == is_video_hole)
294     return;
295
296   is_video_hole_ = is_video_hole;
297   SetPlayerVideoHole();
298 }
299
300 void TizenRendererImpl::SetPlayerVideoHole() {
301   if (media_player_)
302     media_player_->SetVideoHole(is_video_hole_);
303 }
304
305 void TizenRendererImpl::SetMediaGeometry(const gfx::RectF& rect) {
306   if (video_rect_ == rect)
307     return;
308
309   video_rect_ = rect;
310   SetPlayerMediaGeometry();
311 }
312
313 void TizenRendererImpl::SetPlayerMediaGeometry() {
314   if (media_player_) {
315     // Always get the latest viewport rect.
316     media_player_->SetMediaGeometry(GetViewportRect(), video_rect_);
317   }
318 }
319
320 gfx::Rect TizenRendererImpl::GetViewportRect() const {
321   content::WebContents* web_contents = GetWebContents();
322   if (!web_contents)
323     return gfx::Rect();
324
325   if (RenderWidgetHostViewAura* rwhv = static_cast<RenderWidgetHostViewAura*>(
326           web_contents->GetRenderWidgetHostView())) {
327     if (rwhv->offscreen_helper()) {
328       // offscreen rendering mode
329       return rwhv->offscreen_helper()->GetViewBoundsInPix();
330     } else {
331       // onscreen rendering mode
332       return rwhv->window()->bounds();
333     }
334   }
335   return gfx::Rect();
336 }
337
338 void TizenRendererImpl::OnWebViewMoved() {
339   if (media_player_) {
340     LOG(INFO) << __func__ << " Reset WebView-Position.";
341     SetPlayerMediaGeometry();
342   }
343 }
344 #endif
345
346 content::WebContents* TizenRendererImpl::GetWebContents() const {
347   return WebContents::FromRenderFrameHost(
348       RenderFrameHost::FromID(render_process_id_, routing_id_));
349 }
350
351 void TizenRendererImpl::Suspend() {
352   LOG(INFO) << "(" << static_cast<void*>(this) << ") " << __func__;
353   if (is_suspended_)
354     return;
355
356   if (!media_player_) {
357     LOG(INFO) << "media_player_ is not created yet";
358     return;
359   }
360
361   media::MediaPlayerRegistry::GetInstance()->DeactivateMediaPlayer(
362       player_id_, !resource_conflicted_);
363   media_player_->Release();
364   is_suspended_ = true;
365 }
366
367 #if BUILDFLAG(IS_TIZEN_TV)
368 content::WebContentsDelegate* TizenRendererImpl::GetWebContentsDelegate()
369     const {
370   content::WebContents* web_contents = GetWebContents();
371   if (!web_contents) {
372     LOG(ERROR) << "web_contents is nullptr";
373     return nullptr;
374   }
375   return web_contents->GetDelegate();
376 }
377 #endif
378
379 void TizenRendererImpl::ToggleFullscreenMode(bool is_fullscreen,
380                                              ToggledFullscreenCB cb) {
381   if (media_player_)
382     media_player_->ToggleFullscreenMode(is_fullscreen);
383   std::move(cb).Run();
384 }
385
386 void TizenRendererImpl::StartPlayingFrom(base::TimeDelta time) {
387   LOG(INFO) << "(" << static_cast<void*>(this) << ") " << __func__
388             << " time : " << time.InMicroseconds();
389   DCHECK(media_player_);
390   DCHECK(task_runner_->BelongsToCurrentThread());
391   TRACE_EVENT1("media", "TizenRendererImpl::StartPlayingFrom", "time_us",
392                time.InMicroseconds());
393
394   SetPlayerInitialize();
395   SetPlayerPrepare();
396
397   state_ = STATE_INITIALIZING;
398   media_player_->Seek(time, base::OnceClosure());
399   state_ = STATE_PLAYING;
400 }
401
402 void TizenRendererImpl::SetPlaybackRate(double playback_rate) {
403   LOG(INFO) << "(" << static_cast<void*>(this) << ") " << __func__ << " "
404             << playback_rate << "/ " << playback_rate_;
405   DCHECK(task_runner_->BelongsToCurrentThread());
406   TRACE_EVENT1("media", "TizenRendererImpl::SetPlaybackRate", "rate",
407                playback_rate);
408
409   if (playback_rate_ == playback_rate)
410     return;
411
412   playback_rate_ = playback_rate;
413
414   if (playback_rate > 0) {
415     media_player_->SetRate(playback_rate_);
416     media_player_->Play();
417   } else {
418     media_player_->Pause();
419   }
420 }
421
422 void TizenRendererImpl::SetVolume(float volume) {
423   LOG(INFO) << "(" << static_cast<void*>(this) << ") " << __func__;
424   DCHECK(task_runner_->BelongsToCurrentThread());
425   if (volume_ == volume)
426     return;
427
428   volume_ = volume;
429
430   // |SetVolume| could be called before initializing.
431   if (media_player_)
432     SetPlayerVolume();
433 }
434
435 void TizenRendererImpl::SetPlayerVolume() {
436   media_player_->SetVolume(volume_);
437 }
438
439 base::TimeDelta TizenRendererImpl::GetMediaTime() {
440   return media_player_->GetCurrentTime();
441 }
442
443 media::RendererType TizenRendererImpl::GetRendererType() {
444   return media::RendererType::kMediaPlayer;
445 }
446
447 void TizenRendererImpl::OnSelectedVideoTracksChanged(
448     const std::vector<media::DemuxerStream*>& enabled_tracks,
449     base::OnceClosure change_completed_cb) {
450   NOTIMPLEMENTED();
451 }
452
453 void TizenRendererImpl::OnEnabledAudioTracksChanged(
454     const std::vector<media::DemuxerStream*>& enabled_tracks,
455     base::OnceClosure change_completed_cb) {
456   NOTIMPLEMENTED();
457 }
458
459 void TizenRendererImpl::OnWebContentsDestroyed() {
460   web_contents_observer_ = nullptr;
461 }
462
463 void TizenRendererImpl::OnEnded() {
464   playback_rate_ = 0.0;
465   client_->OnEnded();
466 }
467
468 void TizenRendererImpl::OnError(media::PipelineStatus error) {
469   playback_rate_ = 0.0;
470   state_ = STATE_ERROR;
471   client_->OnError(error);
472 }
473
474 void TizenRendererImpl::OnStatisticsUpdate(
475     const media::PipelineStatistics& stats) {
476   NOTIMPLEMENTED();
477 }
478 #if BUILDFLAG(IS_TIZEN_TV)
479 void TizenRendererImpl::SetContentMimeType(const std::string& mime_type) {
480   mime_type_ = mime_type;
481   if (media_player_)
482     media_player_->SetContentMimeType(mime_type);
483 }
484
485 void TizenRendererImpl::SetParentalRatingResult(bool is_pass) {
486   if (media_player_)
487     media_player_->SetParentalRatingResult(is_pass);
488   else
489     LOG(ERROR) << "media_player_ is null";
490 }
491
492 bool TizenRendererImpl::PlaybackNotificationEnabled() {
493   content::WebContents* web_contents = GetWebContents();
494   if (!web_contents) {
495     LOG(ERROR) << "web_contents is nullptr";
496     return false;
497   }
498   blink::web_pref::WebPreferences web_preference =
499       web_contents->GetOrCreateWebPreferences();
500   bool enable = web_preference.media_playback_notification_enabled;
501   LOG(INFO) << "media_playback_notification_enabled:" << enable;
502   return enable;
503 }
504
505 void TizenRendererImpl::NotifyPlaybackState(int state,
506                                             int player_id,
507                                             const std::string& url,
508                                             const std::string& mime_type,
509                                             bool* media_resource_acquired,
510                                             std::string* translated_url,
511                                             std::string* drm_info) {
512   if (!PlaybackNotificationEnabled())
513     return;
514   content::WebContentsDelegate* web_contents_delegate =
515       GetWebContentsDelegate();
516   if (!web_contents_delegate) {
517     LOG(ERROR) << "GetWebContentsDelegate failed";
518     return;
519   }
520
521   if (notify_playback_state_ < media::kPlaybackReady &&
522       state == media::kPlaybackStop) {
523     LOG(ERROR) << "player not Ready but notify Stop";
524   }
525
526   notify_playback_state_ = state;
527   web_contents_delegate->NotifyPlaybackState(state, player_id, url, mime_type,
528                                              media_resource_acquired,
529                                              translated_url, drm_info);
530 }
531
532 void TizenRendererImpl::OnLivePlaybackComplete() {
533   if (!client_) {
534     LOG(ERROR) << "client is not exist";
535     return;
536   }
537   client_->OnLivePlaybackComplete();
538 }
539 #endif
540
541 void TizenRendererImpl::OnSeekableTimeChange(base::TimeDelta min_time,
542                                              base::TimeDelta max_time,
543                                              bool is_live) {
544   client_->OnSeekableTimeChange(min_time, max_time, is_live);
545 }
546
547 void TizenRendererImpl::OnBufferingStateChange(
548     media::BufferingState new_buffering_state,
549     media::BufferingStateChangeReason reason) {
550   client_->OnBufferingStateChange(new_buffering_state, reason);
551 }
552
553 void TizenRendererImpl::OnWaiting(media::WaitingReason reason) {
554   client_->OnWaiting(reason);
555 }
556
557 void TizenRendererImpl::OnAudioConfigChange(
558     const media::AudioDecoderConfig& config) {
559   DCHECK(task_runner_->BelongsToCurrentThread());
560   client_->OnAudioConfigChange(config);
561 }
562
563 void TizenRendererImpl::OnVideoConfigChange(
564     const media::VideoDecoderConfig& config) {
565   DCHECK(task_runner_->BelongsToCurrentThread());
566   client_->OnVideoConfigChange(config);
567 }
568
569 void TizenRendererImpl::OnVideoNaturalSizeChange(const gfx::Size& size) {
570   client_->OnVideoNaturalSizeChange(size);
571 }
572
573 void TizenRendererImpl::OnVideoOpacityChange(bool opaque) {
574   NOTIMPLEMENTED();
575 }
576
577 void TizenRendererImpl::OnVideoFrameRateChange(absl::optional<int> fps) {
578   NOTIMPLEMENTED();
579 }
580
581 void TizenRendererImpl::InitiateScopedSurfaceRequest(
582     InitiateScopedSurfaceRequestCallback callback) {
583   NOTIMPLEMENTED();
584 }
585
586 void TizenRendererImpl::OnVideoSizeChange(const gfx::Size& size) {
587   client_extension_->OnVideoSizeChange(size);
588 }
589
590 void TizenRendererImpl::OnDurationChange(base::TimeDelta duration) {
591   client_extension_->OnDurationChange(duration);
592 }
593
594 void TizenRendererImpl::OnBufferUpdate(base::TimeDelta time) {
595   client_extension_->OnBufferUpdate(time);
596 }
597
598 void TizenRendererImpl::OnRequestSeek(base::TimeDelta time) {
599   client_->OnRequestSeek(time);
600 }
601
602 void TizenRendererImpl::OnRequestSuspend(bool resource_conflicted) {
603   LOG(INFO) << "(" << static_cast<void*>(this) << ") " << __func__;
604
605   if (is_suspended_) {
606     LOG(INFO) << " Media is already suspended.";
607     return;
608   }
609
610   resource_conflicted_ = resource_conflicted;
611   media::MediaPlayerRegistry::GetInstance()->DeactivateMediaPlayer(
612       player_id_, !resource_conflicted_);
613   is_suspended_ = true;
614   client_->OnRequestSuspend(resource_conflicted);
615 }
616
617 #if defined(TIZEN_TBM_SUPPORT)
618 void TizenRendererImpl::OnNewTbmFrameAvailable(uint32_t player_id,
619                                                gfx::TbmBufferHandle tbm_handle,
620                                                base::TimeDelta timestamp) {
621   client_extension_->OnNewTbmFrameAvailable(player_id, tbm_handle, timestamp);
622 }
623
624 void TizenRendererImpl::OnTbmBufferExhausted(
625     const gfx::TbmBufferHandle& tbm_handle) {
626   media_player_->DestroyMediaPacket(
627       reinterpret_cast<void*>(tbm_handle.media_packet));
628 }
629 #else
630 void TizenRendererImpl::OnNewFrameAvailable(
631     uint32_t playerId,
632     base::UnsafeSharedMemoryRegion frame,
633     uint32_t size,
634     base::TimeDelta timestamp,
635     uint32_t width,
636     uint32_t height) {
637   client_extension_->OnNewFrameAvailable(playerId, std::move(frame), size,
638                                          timestamp, width, height);
639 }
640 #endif
641
642 }  // namespace content