1 // Copyright 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.
5 #include "content/common/gpu/media/rendering_helper.h"
11 #include "base/bind.h"
12 #include "base/callback_helpers.h"
13 #include "base/command_line.h"
14 #include "base/mac/scoped_nsautorelease_pool.h"
15 #include "base/message_loop/message_loop.h"
16 #include "base/strings/stringize_macros.h"
17 #include "base/synchronization/waitable_event.h"
18 #include "base/time/time.h"
19 #include "ui/gl/gl_context.h"
20 #include "ui/gl/gl_implementation.h"
21 #include "ui/gl/gl_surface.h"
22 #include "ui/gl/gl_surface_egl.h"
23 #include "ui/gl/gl_surface_glx.h"
30 #include "ui/gfx/x/x11_types.h"
33 #if !defined(OS_WIN) && defined(ARCH_CPU_X86_FAMILY)
34 #define GL_VARIANT_GLX 1
36 #define GL_VARIANT_EGL 1
39 // Helper for Shader creation.
40 static void CreateShader(GLuint program,
44 GLuint shader = glCreateShader(type);
45 glShaderSource(shader, 1, &source, &size);
46 glCompileShader(shader);
47 int result = GL_FALSE;
48 glGetShaderiv(shader, GL_COMPILE_STATUS, &result);
51 glGetShaderInfoLog(shader, arraysize(log), NULL, log);
54 glAttachShader(program, shader);
55 glDeleteShader(shader);
56 CHECK_EQ(static_cast<int>(glGetError()), GL_NO_ERROR);
61 RenderingHelperParams::RenderingHelperParams()
62 : rendering_fps(0), warm_up_iterations(0), render_as_thumbnails(false) {
65 RenderingHelperParams::~RenderingHelperParams() {}
67 VideoFrameTexture::VideoFrameTexture(uint32 texture_target,
69 const base::Closure& no_longer_needed_cb)
70 : texture_target_(texture_target),
71 texture_id_(texture_id),
72 no_longer_needed_cb_(no_longer_needed_cb) {
73 DCHECK(!no_longer_needed_cb_.is_null());
76 VideoFrameTexture::~VideoFrameTexture() {
77 base::ResetAndReturn(&no_longer_needed_cb_).Run();
80 RenderingHelper::RenderedVideo::RenderedVideo()
81 : is_flushing(false), frames_to_drop(0) {
84 RenderingHelper::RenderedVideo::~RenderedVideo() {
88 bool RenderingHelper::InitializeOneOff() {
89 base::CommandLine* cmd_line = base::CommandLine::ForCurrentProcess();
91 cmd_line->AppendSwitchASCII(switches::kUseGL,
92 gfx::kGLImplementationDesktopName);
94 cmd_line->AppendSwitchASCII(switches::kUseGL, gfx::kGLImplementationEGLName);
96 return gfx::GLSurface::InitializeOneOff();
99 RenderingHelper::RenderingHelper() {
100 window_ = gfx::kNullAcceleratedWidget;
104 RenderingHelper::~RenderingHelper() {
105 CHECK_EQ(videos_.size(), 0U) << "Must call UnInitialize before dtor.";
109 void RenderingHelper::Initialize(const RenderingHelperParams& params,
110 base::WaitableEvent* done) {
111 // Use videos_.size() != 0 as a proxy for the class having already been
112 // Initialize()'d, and UnInitialize() before continuing.
113 if (videos_.size()) {
114 base::WaitableEvent done(false, false);
120 base::Bind(&RenderingHelper::RenderContent, base::Unretained(this)));
122 frame_duration_ = params.rendering_fps > 0
123 ? base::TimeDelta::FromSeconds(1) / params.rendering_fps
126 render_as_thumbnails_ = params.render_as_thumbnails;
127 message_loop_ = base::MessageLoop::current();
131 gfx::Size(GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN));
132 window_ = CreateWindowEx(0,
134 L"VideoDecodeAcceleratorTest",
135 WS_OVERLAPPEDWINDOW | WS_VISIBLE,
138 screen_size_.width(),
139 screen_size_.height(),
144 #elif defined(USE_X11)
145 Display* display = gfx::GetXDisplay();
146 Screen* screen = DefaultScreenOfDisplay(display);
147 screen_size_ = gfx::Size(XWidthOfScreen(screen), XHeightOfScreen(screen));
151 XSetWindowAttributes window_attributes;
152 memset(&window_attributes, 0, sizeof(window_attributes));
153 window_attributes.background_pixel =
154 BlackPixel(display, DefaultScreen(display));
155 window_attributes.override_redirect = true;
156 int depth = DefaultDepth(display, DefaultScreen(display));
158 window_ = XCreateWindow(display,
159 DefaultRootWindow(display),
162 screen_size_.width(),
163 screen_size_.height(),
164 0 /* border width */,
166 CopyFromParent /* class */,
167 CopyFromParent /* visual */,
168 (CWBackPixel | CWOverrideRedirect),
170 XStoreName(display, window_, "VideoDecodeAcceleratorTest");
171 XSelectInput(display, window_, ExposureMask);
172 XMapWindow(display, window_);
174 #error unknown platform
176 CHECK(window_ != gfx::kNullAcceleratedWidget);
178 gl_surface_ = gfx::GLSurface::CreateViewGLSurface(window_);
179 gl_context_ = gfx::GLContext::CreateGLContext(
180 NULL, gl_surface_.get(), gfx::PreferIntegratedGpu);
181 gl_context_->MakeCurrent(gl_surface_.get());
183 CHECK_GT(params.window_sizes.size(), 0U);
184 videos_.resize(params.window_sizes.size());
185 LayoutRenderingAreas(params.window_sizes);
187 if (render_as_thumbnails_) {
188 CHECK_EQ(videos_.size(), 1U);
190 GLint max_texture_size;
191 glGetIntegerv(GL_MAX_TEXTURE_SIZE, &max_texture_size);
192 CHECK_GE(max_texture_size, params.thumbnails_page_size.width());
193 CHECK_GE(max_texture_size, params.thumbnails_page_size.height());
195 thumbnails_fbo_size_ = params.thumbnails_page_size;
196 thumbnail_size_ = params.thumbnail_size;
198 glGenFramebuffersEXT(1, &thumbnails_fbo_id_);
199 glGenTextures(1, &thumbnails_texture_id_);
200 glBindTexture(GL_TEXTURE_2D, thumbnails_texture_id_);
201 glTexImage2D(GL_TEXTURE_2D,
204 thumbnails_fbo_size_.width(),
205 thumbnails_fbo_size_.height(),
208 GL_UNSIGNED_SHORT_5_6_5,
210 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
211 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
212 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
213 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
214 glBindTexture(GL_TEXTURE_2D, 0);
216 glBindFramebufferEXT(GL_FRAMEBUFFER, thumbnails_fbo_id_);
217 glFramebufferTexture2DEXT(GL_FRAMEBUFFER,
218 GL_COLOR_ATTACHMENT0,
220 thumbnails_texture_id_,
223 GLenum fb_status = glCheckFramebufferStatusEXT(GL_FRAMEBUFFER);
224 CHECK(fb_status == GL_FRAMEBUFFER_COMPLETE) << fb_status;
225 glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
226 glClear(GL_COLOR_BUFFER_BIT);
227 glBindFramebufferEXT(GL_FRAMEBUFFER, 0);
230 // These vertices and texture coords. map (0,0) in the texture to the
231 // bottom left of the viewport. Since we get the video frames with the
232 // the top left at (0,0) we need to flip the texture y coordinate
233 // in the vertex shader for this to be rendered the right way up.
234 // In the case of thumbnail rendering we use the same vertex shader
235 // to render the FBO the screen, where we do not want this flipping.
236 static const float kVertices[] =
237 { -1.f, 1.f, -1.f, -1.f, 1.f, 1.f, 1.f, -1.f, };
238 static const float kTextureCoords[] = { 0, 1, 0, 0, 1, 1, 1, 0, };
239 static const char kVertexShader[] = STRINGIZE(
240 varying vec2 interp_tc;
241 attribute vec4 in_pos;
242 attribute vec2 in_tc;
243 uniform bool tex_flip;
246 interp_tc = vec2(in_tc.x, 1.0 - in_tc.y);
249 gl_Position = in_pos;
253 static const char kFragmentShader[] =
254 "#extension GL_OES_EGL_image_external : enable\n"
255 "precision mediump float;\n"
256 "varying vec2 interp_tc;\n"
257 "uniform sampler2D tex;\n"
258 "#ifdef GL_OES_EGL_image_external\n"
259 "uniform samplerExternalOES tex_external;\n"
262 " vec4 color = texture2D(tex, interp_tc);\n"
263 "#ifdef GL_OES_EGL_image_external\n"
264 " color += texture2D(tex_external, interp_tc);\n"
266 " gl_FragColor = color;\n"
269 static const char kFragmentShader[] = STRINGIZE(
270 varying vec2 interp_tc;
271 uniform sampler2D tex;
273 gl_FragColor = texture2D(tex, interp_tc);
276 program_ = glCreateProgram();
278 program_, GL_VERTEX_SHADER, kVertexShader, arraysize(kVertexShader));
279 CreateShader(program_,
282 arraysize(kFragmentShader));
283 glLinkProgram(program_);
284 int result = GL_FALSE;
285 glGetProgramiv(program_, GL_LINK_STATUS, &result);
288 glGetShaderInfoLog(program_, arraysize(log), NULL, log);
291 glUseProgram(program_);
292 glDeleteProgram(program_);
294 glUniform1i(glGetUniformLocation(program_, "tex_flip"), 0);
295 glUniform1i(glGetUniformLocation(program_, "tex"), 0);
296 GLint tex_external = glGetUniformLocation(program_, "tex_external");
297 if (tex_external != -1) {
298 glUniform1i(tex_external, 1);
300 int pos_location = glGetAttribLocation(program_, "in_pos");
301 glEnableVertexAttribArray(pos_location);
302 glVertexAttribPointer(pos_location, 2, GL_FLOAT, GL_FALSE, 0, kVertices);
303 int tc_location = glGetAttribLocation(program_, "in_tc");
304 glEnableVertexAttribArray(tc_location);
305 glVertexAttribPointer(tc_location, 2, GL_FLOAT, GL_FALSE, 0, kTextureCoords);
307 if (frame_duration_ != base::TimeDelta())
308 WarmUpRendering(params.warm_up_iterations);
310 // It's safe to use Unretained here since |rendering_thread_| will be stopped
311 // in VideoDecodeAcceleratorTest.TearDown(), while the |rendering_helper_| is
312 // a member of that class. (See video_decode_accelerator_unittest.cc.)
313 gl_surface_->GetVSyncProvider()->GetVSyncParameters(base::Bind(
314 &RenderingHelper::UpdateVSyncParameters, base::Unretained(this), done));
317 // The rendering for the first few frames is slow (e.g., 100ms on Peach Pit).
318 // This affects the numbers measured in the performance test. We try to render
319 // several frames here to warm up the rendering.
320 void RenderingHelper::WarmUpRendering(int warm_up_iterations) {
321 unsigned int texture_id;
322 scoped_ptr<GLubyte[]> emptyData(new GLubyte[screen_size_.GetArea() * 2]);
323 glGenTextures(1, &texture_id);
324 glBindTexture(GL_TEXTURE_2D, texture_id);
325 glTexImage2D(GL_TEXTURE_2D,
328 screen_size_.width(),
329 screen_size_.height(),
332 GL_UNSIGNED_SHORT_5_6_5,
334 for (int i = 0; i < warm_up_iterations; ++i) {
335 RenderTexture(GL_TEXTURE_2D, texture_id);
336 gl_surface_->SwapBuffers();
338 glDeleteTextures(1, &texture_id);
341 void RenderingHelper::UnInitialize(base::WaitableEvent* done) {
342 CHECK_EQ(base::MessageLoop::current(), message_loop_);
344 render_task_.Cancel();
346 if (render_as_thumbnails_) {
347 glDeleteTextures(1, &thumbnails_texture_id_);
348 glDeleteFramebuffersEXT(1, &thumbnails_fbo_id_);
351 gl_context_->ReleaseCurrent(gl_surface_.get());
359 void RenderingHelper::CreateTexture(uint32 texture_target,
361 const gfx::Size& size,
362 base::WaitableEvent* done) {
363 if (base::MessageLoop::current() != message_loop_) {
364 message_loop_->PostTask(FROM_HERE,
365 base::Bind(&RenderingHelper::CreateTexture,
366 base::Unretained(this),
373 glGenTextures(1, texture_id);
374 glBindTexture(texture_target, *texture_id);
375 if (texture_target == GL_TEXTURE_2D) {
376 glTexImage2D(GL_TEXTURE_2D,
386 glTexParameteri(texture_target, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
387 glTexParameteri(texture_target, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
388 // OpenGLES2.0.25 section 3.8.2 requires CLAMP_TO_EDGE for NPOT textures.
389 glTexParameteri(texture_target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
390 glTexParameteri(texture_target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
391 CHECK_EQ(static_cast<int>(glGetError()), GL_NO_ERROR);
395 // Helper function to set GL viewport.
396 static inline void GLSetViewPort(const gfx::Rect& area) {
397 glViewport(area.x(), area.y(), area.width(), area.height());
398 glScissor(area.x(), area.y(), area.width(), area.height());
401 void RenderingHelper::RenderThumbnail(uint32 texture_target,
403 CHECK_EQ(base::MessageLoop::current(), message_loop_);
404 const int width = thumbnail_size_.width();
405 const int height = thumbnail_size_.height();
406 const int thumbnails_in_row = thumbnails_fbo_size_.width() / width;
407 const int thumbnails_in_column = thumbnails_fbo_size_.height() / height;
408 const int row = (frame_count_ / thumbnails_in_row) % thumbnails_in_column;
409 const int col = frame_count_ % thumbnails_in_row;
411 gfx::Rect area(col * width, row * height, width, height);
413 glUniform1i(glGetUniformLocation(program_, "tex_flip"), 0);
414 glBindFramebufferEXT(GL_FRAMEBUFFER, thumbnails_fbo_id_);
416 RenderTexture(texture_target, texture_id);
417 glBindFramebufferEXT(GL_FRAMEBUFFER, 0);
419 // Need to flush the GL commands before we return the tnumbnail texture to
425 void RenderingHelper::QueueVideoFrame(
427 scoped_refptr<VideoFrameTexture> video_frame) {
428 CHECK_EQ(base::MessageLoop::current(), message_loop_);
429 RenderedVideo* video = &videos_[window_id];
430 DCHECK(!video->is_flushing);
432 video->pending_frames.push(video_frame);
434 if (video->frames_to_drop > 0 && video->pending_frames.size() > 1) {
435 --video->frames_to_drop;
436 video->pending_frames.pop();
439 // Schedules the first RenderContent() if need.
440 if (scheduled_render_time_.is_null()) {
441 scheduled_render_time_ = base::TimeTicks::Now();
442 message_loop_->PostTask(FROM_HERE, render_task_.callback());
446 void RenderingHelper::RenderTexture(uint32 texture_target, uint32 texture_id) {
447 // The ExternalOES sampler is bound to GL_TEXTURE1 and the Texture2D sampler
448 // is bound to GL_TEXTURE0.
449 if (texture_target == GL_TEXTURE_2D) {
450 glActiveTexture(GL_TEXTURE0 + 0);
451 } else if (texture_target == GL_TEXTURE_EXTERNAL_OES) {
452 glActiveTexture(GL_TEXTURE0 + 1);
454 glBindTexture(texture_target, texture_id);
455 glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
456 glBindTexture(texture_target, 0);
457 CHECK_EQ(static_cast<int>(glGetError()), GL_NO_ERROR);
460 void RenderingHelper::DeleteTexture(uint32 texture_id) {
461 CHECK_EQ(base::MessageLoop::current(), message_loop_);
462 glDeleteTextures(1, &texture_id);
463 CHECK_EQ(static_cast<int>(glGetError()), GL_NO_ERROR);
466 void* RenderingHelper::GetGLContext() {
467 return gl_context_->GetHandle();
470 void* RenderingHelper::GetGLDisplay() {
471 return gl_surface_->GetDisplay();
474 void RenderingHelper::Clear() {
476 message_loop_ = NULL;
480 render_as_thumbnails_ = false;
482 thumbnails_fbo_id_ = 0;
483 thumbnails_texture_id_ = 0;
487 DestroyWindow(window_);
489 // Destroy resources acquired in Initialize, in reverse-acquisition order.
491 CHECK(XUnmapWindow(gfx::GetXDisplay(), window_));
492 CHECK(XDestroyWindow(gfx::GetXDisplay(), window_));
495 window_ = gfx::kNullAcceleratedWidget;
498 void RenderingHelper::GetThumbnailsAsRGB(std::vector<unsigned char>* rgb,
500 base::WaitableEvent* done) {
501 CHECK(render_as_thumbnails_);
503 const size_t num_pixels = thumbnails_fbo_size_.GetArea();
504 std::vector<unsigned char> rgba;
505 rgba.resize(num_pixels * 4);
506 glBindFramebufferEXT(GL_FRAMEBUFFER, thumbnails_fbo_id_);
507 glPixelStorei(GL_PACK_ALIGNMENT, 1);
508 // We can only count on GL_RGBA/GL_UNSIGNED_BYTE support.
511 thumbnails_fbo_size_.width(),
512 thumbnails_fbo_size_.height(),
516 glBindFramebufferEXT(GL_FRAMEBUFFER, 0);
517 rgb->resize(num_pixels * 3);
518 // Drop the alpha channel, but check as we go that it is all 0xff.
520 unsigned char* rgb_ptr = &((*rgb)[0]);
521 unsigned char* rgba_ptr = &rgba[0];
522 for (size_t i = 0; i < num_pixels; ++i) {
523 *rgb_ptr++ = *rgba_ptr++;
524 *rgb_ptr++ = *rgba_ptr++;
525 *rgb_ptr++ = *rgba_ptr++;
526 solid = solid && (*rgba_ptr == 0xff);
529 *alpha_solid = solid;
534 void RenderingHelper::Flush(size_t window_id) {
535 videos_[window_id].is_flushing = true;
538 void RenderingHelper::RenderContent() {
539 CHECK_EQ(base::MessageLoop::current(), message_loop_);
541 // Update the VSync params.
543 // It's safe to use Unretained here since |rendering_thread_| will be stopped
544 // in VideoDecodeAcceleratorTest.TearDown(), while the |rendering_helper_| is
545 // a member of that class. (See video_decode_accelerator_unittest.cc.)
546 gl_surface_->GetVSyncProvider()->GetVSyncParameters(
547 base::Bind(&RenderingHelper::UpdateVSyncParameters,
548 base::Unretained(this),
549 static_cast<base::WaitableEvent*>(NULL)));
551 glUniform1i(glGetUniformLocation(program_, "tex_flip"), 1);
553 // Frames that will be returned to the client (via the no_longer_needed_cb)
554 // after this vector falls out of scope at the end of this method. We need
555 // to keep references to them until after SwapBuffers() call below.
556 std::vector<scoped_refptr<VideoFrameTexture> > frames_to_be_returned;
557 bool need_swap_buffer = false;
558 if (render_as_thumbnails_) {
559 // In render_as_thumbnails_ mode, we render the FBO content on the
560 // screen instead of the decoded textures.
561 GLSetViewPort(videos_[0].render_area);
562 RenderTexture(GL_TEXTURE_2D, thumbnails_texture_id_);
563 need_swap_buffer = true;
565 for (RenderedVideo& video : videos_) {
566 if (video.pending_frames.empty())
568 need_swap_buffer = true;
569 scoped_refptr<VideoFrameTexture> frame = video.pending_frames.front();
570 GLSetViewPort(video.render_area);
571 RenderTexture(frame->texture_target(), frame->texture_id());
573 if (video.pending_frames.size() > 1 || video.is_flushing) {
574 frames_to_be_returned.push_back(video.pending_frames.front());
575 video.pending_frames.pop();
577 ++video.frames_to_drop;
582 if (need_swap_buffer)
583 gl_surface_->SwapBuffers();
585 ScheduleNextRenderContent();
588 // Helper function for the LayoutRenderingAreas(). The |lengths| are the
589 // heights(widths) of the rows(columns). It scales the elements in
590 // |lengths| proportionally so that the sum of them equal to |total_length|.
591 // It also outputs the coordinates of the rows(columns) to |offsets|.
592 static void ScaleAndCalculateOffsets(std::vector<int>* lengths,
593 std::vector<int>* offsets,
595 int sum = std::accumulate(lengths->begin(), lengths->end(), 0);
596 for (size_t i = 0; i < lengths->size(); ++i) {
597 lengths->at(i) = lengths->at(i) * total_length / sum;
598 offsets->at(i) = (i == 0) ? 0 : offsets->at(i - 1) + lengths->at(i - 1);
602 void RenderingHelper::LayoutRenderingAreas(
603 const std::vector<gfx::Size>& window_sizes) {
604 // Find the number of colums and rows.
605 // The smallest n * n or n * (n + 1) > number of windows.
606 size_t cols = sqrt(videos_.size() - 1) + 1;
607 size_t rows = (videos_.size() + cols - 1) / cols;
609 // Find the widths and heights of the grid.
610 std::vector<int> widths(cols);
611 std::vector<int> heights(rows);
612 std::vector<int> offset_x(cols);
613 std::vector<int> offset_y(rows);
615 for (size_t i = 0; i < window_sizes.size(); ++i) {
616 const gfx::Size& size = window_sizes[i];
617 widths[i % cols] = std::max(widths[i % cols], size.width());
618 heights[i / cols] = std::max(heights[i / cols], size.height());
621 ScaleAndCalculateOffsets(&widths, &offset_x, screen_size_.width());
622 ScaleAndCalculateOffsets(&heights, &offset_y, screen_size_.height());
624 // Put each render_area_ in the center of each cell.
625 for (size_t i = 0; i < window_sizes.size(); ++i) {
626 const gfx::Size& size = window_sizes[i];
628 std::min(static_cast<float>(widths[i % cols]) / size.width(),
629 static_cast<float>(heights[i / cols]) / size.height());
631 // Don't scale up the texture.
632 scale = std::min(1.0f, scale);
634 size_t w = scale * size.width();
635 size_t h = scale * size.height();
636 size_t x = offset_x[i % cols] + (widths[i % cols] - w) / 2;
637 size_t y = offset_y[i / cols] + (heights[i / cols] - h) / 2;
638 videos_[i].render_area = gfx::Rect(x, y, w, h);
642 void RenderingHelper::UpdateVSyncParameters(base::WaitableEvent* done,
643 const base::TimeTicks timebase,
644 const base::TimeDelta interval) {
645 vsync_timebase_ = timebase;
646 vsync_interval_ = interval;
652 void RenderingHelper::DropOneFrameForAllVideos() {
653 for (RenderedVideo& video : videos_) {
654 if (video.pending_frames.empty())
657 if (video.pending_frames.size() > 1 || video.is_flushing) {
658 video.pending_frames.pop();
660 ++video.frames_to_drop;
665 void RenderingHelper::ScheduleNextRenderContent() {
666 scheduled_render_time_ += frame_duration_;
668 // Schedules the next RenderContent() at latest VSYNC before the
669 // |scheduled_render_time_|.
670 base::TimeTicks now = base::TimeTicks::Now();
671 base::TimeTicks target =
672 std::max(now + vsync_interval_, scheduled_render_time_);
674 int64 intervals = (target - vsync_timebase_) / vsync_interval_;
675 target = vsync_timebase_ + intervals * vsync_interval_;
677 // When the rendering falls behind, drops frames.
678 while (scheduled_render_time_ < target) {
679 scheduled_render_time_ += frame_duration_;
680 DropOneFrameForAllVideos();
683 message_loop_->PostDelayedTask(
684 FROM_HERE, render_task_.callback(), target - now);
686 } // namespace content