video_capture_controller.cc revision f2477e01787aa58f445919b809d89e252beef54f
1// Copyright (c) 2012 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/browser/renderer_host/media/video_capture_controller.h" 6 7#include <set> 8 9#include "base/bind.h" 10#include "base/debug/trace_event.h" 11#include "base/stl_util.h" 12#include "content/browser/renderer_host/media/media_stream_manager.h" 13#include "content/browser/renderer_host/media/video_capture_manager.h" 14#include "content/public/browser/browser_thread.h" 15#include "media/base/video_frame.h" 16#include "media/base/video_util.h" 17#include "media/base/yuv_convert.h" 18 19#if !defined(AVOID_LIBYUV_FOR_ANDROID_WEBVIEW) 20#include "third_party/libyuv/include/libyuv.h" 21#endif 22 23using media::VideoCaptureFormat; 24 25namespace content { 26 27namespace { 28 29// The number of buffers that VideoCaptureBufferPool should allocate. 30const int kNoOfBuffers = 3; 31 32class PoolBuffer : public media::VideoCaptureDevice::Client::Buffer { 33 public: 34 PoolBuffer(const scoped_refptr<VideoCaptureBufferPool>& pool, 35 int buffer_id, 36 void* data, 37 size_t size) 38 : Buffer(buffer_id, data, size), pool_(pool) { 39 DCHECK(pool_); 40 } 41 42 private: 43 virtual ~PoolBuffer() { pool_->RelinquishProducerReservation(id()); } 44 45 const scoped_refptr<VideoCaptureBufferPool> pool_; 46}; 47 48} // anonymous namespace 49 50struct VideoCaptureController::ControllerClient { 51 ControllerClient(const VideoCaptureControllerID& id, 52 VideoCaptureControllerEventHandler* handler, 53 base::ProcessHandle render_process, 54 media::VideoCaptureSessionId session_id, 55 const media::VideoCaptureParams& params) 56 : controller_id(id), 57 event_handler(handler), 58 render_process_handle(render_process), 59 session_id(session_id), 60 parameters(params), 61 session_closed(false) {} 62 63 ~ControllerClient() {} 64 65 // ID used for identifying this object. 66 const VideoCaptureControllerID controller_id; 67 VideoCaptureControllerEventHandler* const event_handler; 68 69 // Handle to the render process that will receive the capture buffers. 70 const base::ProcessHandle render_process_handle; 71 const media::VideoCaptureSessionId session_id; 72 const media::VideoCaptureParams parameters; 73 74 // Buffers that are currently known to this client. 75 std::set<int> known_buffers; 76 77 // Buffers currently held by this client. 78 std::set<int> active_buffers; 79 80 // State of capture session, controlled by VideoCaptureManager directly. This 81 // transitions to true as soon as StopSession() occurs, at which point the 82 // client is sent an OnEnded() event. However, because the client retains a 83 // VideoCaptureController* pointer, its ControllerClient entry lives on until 84 // it unregisters itself via RemoveClient(), which may happen asynchronously. 85 // 86 // TODO(nick): If we changed the semantics of VideoCaptureHost so that 87 // OnEnded() events were processed synchronously (with the RemoveClient() done 88 // implicitly), we could avoid tracking this state here in the Controller, and 89 // simplify the code in both places. 90 bool session_closed; 91}; 92 93// Receives events from the VideoCaptureDevice and posts them to a 94// VideoCaptureController on the IO thread. An instance of this class may safely 95// outlive its target VideoCaptureController. 96// 97// Methods of this class may be called from any thread, and in practice will 98// often be called on some auxiliary thread depending on the platform and the 99// device type; including, for example, the DirectShow thread on Windows, the 100// v4l2_thread on Linux, and the UI thread for tab capture. 101class VideoCaptureController::VideoCaptureDeviceClient 102 : public media::VideoCaptureDevice::Client { 103 public: 104 explicit VideoCaptureDeviceClient( 105 const base::WeakPtr<VideoCaptureController>& controller, 106 const scoped_refptr<VideoCaptureBufferPool>& buffer_pool); 107 virtual ~VideoCaptureDeviceClient(); 108 109 // VideoCaptureDevice::Client implementation. 110 virtual scoped_refptr<Buffer> ReserveOutputBuffer( 111 media::VideoFrame::Format format, 112 const gfx::Size& size) OVERRIDE; 113 virtual void OnIncomingCapturedFrame(const uint8* data, 114 int length, 115 base::Time timestamp, 116 int rotation, 117 bool flip_vert, 118 bool flip_horiz, 119 const VideoCaptureFormat& frame_format) 120 OVERRIDE; 121 virtual void OnIncomingCapturedBuffer(const scoped_refptr<Buffer>& buffer, 122 media::VideoFrame::Format format, 123 const gfx::Size& dimensions, 124 base::Time timestamp, 125 int frame_rate) OVERRIDE; 126 virtual void OnError() OVERRIDE; 127 128 private: 129 scoped_refptr<Buffer> DoReserveOutputBuffer(media::VideoFrame::Format format, 130 const gfx::Size& dimensions, 131 int rotation); 132 133 // The controller to which we post events. 134 const base::WeakPtr<VideoCaptureController> controller_; 135 136 // The pool of shared-memory buffers used for capturing. 137 const scoped_refptr<VideoCaptureBufferPool> buffer_pool_; 138 139 // The set of buffers that have been used for rotated capturing. 140 std::set<int> rotated_buffers_; 141}; 142 143VideoCaptureController::VideoCaptureController() 144 : buffer_pool_(new VideoCaptureBufferPool(kNoOfBuffers)), 145 state_(VIDEO_CAPTURE_STATE_STARTED), 146 weak_ptr_factory_(this) { 147} 148 149VideoCaptureController::VideoCaptureDeviceClient::VideoCaptureDeviceClient( 150 const base::WeakPtr<VideoCaptureController>& controller, 151 const scoped_refptr<VideoCaptureBufferPool>& buffer_pool) 152 : controller_(controller), buffer_pool_(buffer_pool) {} 153 154VideoCaptureController::VideoCaptureDeviceClient::~VideoCaptureDeviceClient() {} 155 156base::WeakPtr<VideoCaptureController> VideoCaptureController::GetWeakPtr() { 157 return weak_ptr_factory_.GetWeakPtr(); 158} 159 160scoped_ptr<media::VideoCaptureDevice::Client> 161VideoCaptureController::NewDeviceClient() { 162 scoped_ptr<media::VideoCaptureDevice::Client> result( 163 new VideoCaptureDeviceClient(this->GetWeakPtr(), buffer_pool_)); 164 return result.Pass(); 165} 166 167void VideoCaptureController::AddClient( 168 const VideoCaptureControllerID& id, 169 VideoCaptureControllerEventHandler* event_handler, 170 base::ProcessHandle render_process, 171 media::VideoCaptureSessionId session_id, 172 const media::VideoCaptureParams& params) { 173 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); 174 DVLOG(1) << "VideoCaptureController::AddClient, id " << id.device_id 175 << ", " << params.requested_format.frame_size.ToString() 176 << ", " << params.requested_format.frame_rate 177 << ", " << session_id 178 << ")"; 179 180 // Signal error in case device is already in error state. 181 if (state_ == VIDEO_CAPTURE_STATE_ERROR) { 182 event_handler->OnError(id); 183 return; 184 } 185 186 // Do nothing if this client has called AddClient before. 187 if (FindClient(id, event_handler, controller_clients_)) 188 return; 189 190 ControllerClient* client = new ControllerClient( 191 id, event_handler, render_process, session_id, params); 192 // If we already have gotten frame_info from the device, repeat it to the new 193 // client. 194 if (state_ == VIDEO_CAPTURE_STATE_STARTED) { 195 controller_clients_.push_back(client); 196 return; 197 } 198} 199 200int VideoCaptureController::RemoveClient( 201 const VideoCaptureControllerID& id, 202 VideoCaptureControllerEventHandler* event_handler) { 203 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); 204 DVLOG(1) << "VideoCaptureController::RemoveClient, id " << id.device_id; 205 206 ControllerClient* client = FindClient(id, event_handler, controller_clients_); 207 if (!client) 208 return kInvalidMediaCaptureSessionId; 209 210 // Take back all buffers held by the |client|. 211 for (std::set<int>::iterator buffer_it = client->active_buffers.begin(); 212 buffer_it != client->active_buffers.end(); 213 ++buffer_it) { 214 int buffer_id = *buffer_it; 215 buffer_pool_->RelinquishConsumerHold(buffer_id, 1); 216 } 217 client->active_buffers.clear(); 218 219 int session_id = client->session_id; 220 controller_clients_.remove(client); 221 delete client; 222 223 return session_id; 224} 225 226void VideoCaptureController::StopSession(int session_id) { 227 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); 228 DVLOG(1) << "VideoCaptureController::StopSession, id " << session_id; 229 230 ControllerClient* client = FindClient(session_id, controller_clients_); 231 232 if (client) { 233 client->session_closed = true; 234 client->event_handler->OnEnded(client->controller_id); 235 } 236} 237 238void VideoCaptureController::ReturnBuffer( 239 const VideoCaptureControllerID& id, 240 VideoCaptureControllerEventHandler* event_handler, 241 int buffer_id) { 242 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); 243 244 ControllerClient* client = FindClient(id, event_handler, controller_clients_); 245 246 // If this buffer is not held by this client, or this client doesn't exist 247 // in controller, do nothing. 248 if (!client || !client->active_buffers.erase(buffer_id)) { 249 NOTREACHED(); 250 return; 251 } 252 253 buffer_pool_->RelinquishConsumerHold(buffer_id, 1); 254} 255 256scoped_refptr<media::VideoCaptureDevice::Client::Buffer> 257VideoCaptureController::VideoCaptureDeviceClient::ReserveOutputBuffer( 258 media::VideoFrame::Format format, 259 const gfx::Size& size) { 260 return DoReserveOutputBuffer(format, size, 0); 261} 262 263void VideoCaptureController::VideoCaptureDeviceClient::OnIncomingCapturedFrame( 264 const uint8* data, 265 int length, 266 base::Time timestamp, 267 int rotation, 268 bool flip_vert, 269 bool flip_horiz, 270 const VideoCaptureFormat& frame_format) { 271 TRACE_EVENT0("video", "VideoCaptureController::OnIncomingCapturedFrame"); 272 273 if (!frame_format.IsValid()) 274 return; 275 276 // Chopped pixels in width/height in case video capture device has odd 277 // numbers for width/height. 278 int chopped_width = 0; 279 int chopped_height = 0; 280 int new_width = frame_format.frame_size.width(); 281 int new_height = frame_format.frame_size.height(); 282 283 if (new_width & 1) { 284 --new_width; 285 chopped_width = 1; 286 } 287 if (new_height & 1) { 288 --new_height; 289 chopped_height = 1; 290 } 291 292 const gfx::Size dimensions(new_width, new_height); 293 scoped_refptr<Buffer> buffer = 294 DoReserveOutputBuffer(media::VideoFrame::I420, dimensions, rotation); 295 296 if (!buffer) 297 return; 298#if !defined(AVOID_LIBYUV_FOR_ANDROID_WEBVIEW) 299 uint8* yplane = reinterpret_cast<uint8*>(buffer->data()); 300 uint8* uplane = 301 yplane + 302 media::VideoFrame::PlaneAllocationSize( 303 media::VideoFrame::I420, media::VideoFrame::kYPlane, dimensions); 304 uint8* vplane = 305 uplane + 306 media::VideoFrame::PlaneAllocationSize( 307 media::VideoFrame::I420, media::VideoFrame::kUPlane, dimensions); 308 int yplane_stride = new_width; 309 int uv_plane_stride = new_width / 2; 310 int crop_x = 0; 311 int crop_y = 0; 312 int destination_width = new_width; 313 int destination_height = new_height; 314 libyuv::FourCC origin_colorspace = libyuv::FOURCC_ANY; 315 316 // When rotating by 90 and 270 degrees swap |flip_horiz| and |flip_vert| 317 // because ConvertToI420() flips image before rotation, while 318 // OnIncomingCapturedFrame() interface assumes that rotation happens before 319 // flips. 320 if (rotation == 90 || rotation == 270) 321 std::swap(flip_horiz, flip_vert); 322 323 // Assuming rotation happens first and flips next, we can consolidate both 324 // vertical and horizontal flips together with rotation into two variables: 325 // new_rotation = (rotation + 180 * horizontal_flip) modulo 360 326 // new_vertical_flip = horizontal_flip XOR vertical_flip 327 int new_rotation_angle = (rotation + 180 * flip_horiz) % 360; 328 libyuv::RotationMode rotation_mode = libyuv::kRotate0; 329 if (new_rotation_angle == 90) 330 rotation_mode = libyuv::kRotate90; 331 else if (new_rotation_angle == 180) 332 rotation_mode = libyuv::kRotate180; 333 else if (new_rotation_angle == 270) 334 rotation_mode = libyuv::kRotate270; 335 336 switch (frame_format.pixel_format) { 337 case media::PIXEL_FORMAT_UNKNOWN: // Color format not set. 338 break; 339 case media::PIXEL_FORMAT_I420: 340 DCHECK(!chopped_width && !chopped_height); 341 origin_colorspace = libyuv::FOURCC_I420; 342 break; 343 case media::PIXEL_FORMAT_YV12: 344 DCHECK(!chopped_width && !chopped_height); 345 origin_colorspace = libyuv::FOURCC_YV12; 346 break; 347 case media::PIXEL_FORMAT_NV21: 348 DCHECK(!chopped_width && !chopped_height); 349 origin_colorspace = libyuv::FOURCC_NV12; 350 break; 351 case media::PIXEL_FORMAT_YUY2: 352 DCHECK(!chopped_width && !chopped_height); 353 origin_colorspace = libyuv::FOURCC_YUY2; 354 break; 355 case media::PIXEL_FORMAT_UYVY: 356 DCHECK(!chopped_width && !chopped_height); 357 origin_colorspace = libyuv::FOURCC_UYVY; 358 break; 359 case media::PIXEL_FORMAT_RGB24: 360 origin_colorspace = libyuv::FOURCC_RAW; 361 break; 362 case media::PIXEL_FORMAT_ARGB: 363 origin_colorspace = libyuv::FOURCC_ARGB; 364 break; 365 case media::PIXEL_FORMAT_MJPEG: 366 origin_colorspace = libyuv::FOURCC_MJPG; 367 break; 368 default: 369 NOTREACHED(); 370 } 371 372 int need_convert_rgb24_on_win = false; 373#if defined(OS_WIN) 374 // kRGB24 on Windows start at the bottom line and has a negative stride. This 375 // is not supported by libyuv, so the media API is used instead. 376 if (frame_format.pixel_format == media::PIXEL_FORMAT_RGB24) { 377 // Rotation and flipping is not supported in kRGB24 and OS_WIN case. 378 DCHECK(!rotation && !flip_vert && !flip_horiz); 379 need_convert_rgb24_on_win = true; 380 } 381#endif 382 if (need_convert_rgb24_on_win) { 383 int rgb_stride = -3 * (new_width + chopped_width); 384 const uint8* rgb_src = data + 3 * (new_width + chopped_width) * 385 (new_height - 1 + chopped_height); 386 media::ConvertRGB24ToYUV(rgb_src, 387 yplane, 388 uplane, 389 vplane, 390 new_width, 391 new_height, 392 rgb_stride, 393 yplane_stride, 394 uv_plane_stride); 395 } else { 396 if (new_rotation_angle==90 || new_rotation_angle==270){ 397 // To be compatible with non-libyuv code in RotatePlaneByPixels, when 398 // rotating by 90/270, only the maximum square portion located in the 399 // center of the image is rotated. F.i. 640x480 pixels, only the central 400 // 480 pixels would be rotated and the leftmost and rightmost 80 columns 401 // would be ignored. This process is called letterboxing. 402 int letterbox_thickness = abs(new_width - new_height) / 2; 403 if (destination_width > destination_height) { 404 yplane += letterbox_thickness; 405 uplane += letterbox_thickness / 2; 406 vplane += letterbox_thickness / 2; 407 destination_width = destination_height; 408 } else { 409 yplane += letterbox_thickness * destination_width; 410 uplane += (letterbox_thickness * destination_width) / 2; 411 vplane += (letterbox_thickness * destination_width) / 2; 412 destination_height = destination_width; 413 } 414 } 415 libyuv::ConvertToI420(data, 416 length, 417 yplane, 418 yplane_stride, 419 uplane, 420 uv_plane_stride, 421 vplane, 422 uv_plane_stride, 423 crop_x, 424 crop_y, 425 new_width + chopped_width, 426 new_height * (flip_vert ^ flip_horiz ? -1 : 1), 427 destination_width, 428 destination_height, 429 rotation_mode, 430 origin_colorspace); 431 } 432#else 433 // Libyuv is not linked in for Android WebView builds, but video capture is 434 // not used in those builds either. Whenever libyuv is added in that build, 435 // address all these #ifdef parts, see http://crbug.com/299611 . 436 NOTREACHED(); 437#endif // if !defined(AVOID_LIBYUV_FOR_ANDROID_WEBVIEW) 438 BrowserThread::PostTask( 439 BrowserThread::IO, 440 FROM_HERE, 441 base::Bind( 442 &VideoCaptureController::DoIncomingCapturedI420BufferOnIOThread, 443 controller_, 444 buffer, 445 dimensions, 446 frame_format.frame_rate, 447 timestamp)); 448} 449 450void VideoCaptureController::VideoCaptureDeviceClient::OnIncomingCapturedBuffer( 451 const scoped_refptr<Buffer>& buffer, 452 media::VideoFrame::Format format, 453 const gfx::Size& dimensions, 454 base::Time timestamp, 455 int frame_rate) { 456 // The capture pipeline expects I420 for now. 457 DCHECK_EQ(format, media::VideoFrame::I420) 458 << "Non-I420 output buffer returned"; 459 460 BrowserThread::PostTask( 461 BrowserThread::IO, 462 FROM_HERE, 463 base::Bind( 464 &VideoCaptureController::DoIncomingCapturedI420BufferOnIOThread, 465 controller_, 466 buffer, 467 dimensions, 468 frame_rate, 469 timestamp)); 470} 471 472void VideoCaptureController::VideoCaptureDeviceClient::OnError() { 473 BrowserThread::PostTask(BrowserThread::IO, 474 FROM_HERE, 475 base::Bind(&VideoCaptureController::DoErrorOnIOThread, controller_)); 476} 477 478scoped_refptr<media::VideoCaptureDevice::Client::Buffer> 479VideoCaptureController::VideoCaptureDeviceClient::DoReserveOutputBuffer( 480 media::VideoFrame::Format format, 481 const gfx::Size& dimensions, 482 int rotation) { 483 // The capture pipeline expects I420 for now. 484 DCHECK_EQ(format, media::VideoFrame::I420) 485 << "Non-I420 output buffer requested"; 486 487 int buffer_id_to_drop = VideoCaptureBufferPool::kInvalidId; 488 const size_t frame_bytes = 489 media::VideoFrame::AllocationSize(format, dimensions); 490 491 int buffer_id = 492 buffer_pool_->ReserveForProducer(frame_bytes, &buffer_id_to_drop); 493 if (buffer_id == VideoCaptureBufferPool::kInvalidId) 494 return NULL; 495 void* data; 496 size_t size; 497 buffer_pool_->GetBufferInfo(buffer_id, &data, &size); 498 499 scoped_refptr<media::VideoCaptureDevice::Client::Buffer> output_buffer( 500 new PoolBuffer(buffer_pool_, buffer_id, data, size)); 501 502 if (buffer_id_to_drop != VideoCaptureBufferPool::kInvalidId) { 503 BrowserThread::PostTask(BrowserThread::IO, 504 FROM_HERE, 505 base::Bind(&VideoCaptureController::DoBufferDestroyedOnIOThread, 506 controller_, buffer_id_to_drop)); 507 rotated_buffers_.erase(buffer_id_to_drop); 508 } 509 510 // If a 90/270 rotation is required, letterboxing will be required. If the 511 // returned frame has not been rotated before, then the letterbox borders will 512 // not yet have been cleared and we should clear them now. 513 if ((rotation % 180) == 0) { 514 rotated_buffers_.erase(buffer_id); 515 } else { 516 if (rotated_buffers_.insert(buffer_id).second) 517 memset(output_buffer->data(), 0, output_buffer->size()); 518 } 519 520 return output_buffer; 521} 522 523VideoCaptureController::~VideoCaptureController() { 524 STLDeleteContainerPointers(controller_clients_.begin(), 525 controller_clients_.end()); 526} 527 528void VideoCaptureController::DoIncomingCapturedI420BufferOnIOThread( 529 scoped_refptr<media::VideoCaptureDevice::Client::Buffer> buffer, 530 const gfx::Size& dimensions, 531 int frame_rate, 532 base::Time timestamp) { 533 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); 534 DCHECK_NE(buffer->id(), VideoCaptureBufferPool::kInvalidId); 535 536 VideoCaptureFormat frame_format( 537 dimensions, frame_rate, media::PIXEL_FORMAT_I420); 538 539 int count = 0; 540 if (state_ == VIDEO_CAPTURE_STATE_STARTED) { 541 for (ControllerClients::iterator client_it = controller_clients_.begin(); 542 client_it != controller_clients_.end(); ++client_it) { 543 ControllerClient* client = *client_it; 544 if (client->session_closed) 545 continue; 546 547 bool is_new_buffer = client->known_buffers.insert(buffer->id()).second; 548 if (is_new_buffer) { 549 // On the first use of a buffer on a client, share the memory handle. 550 size_t memory_size = 0; 551 base::SharedMemoryHandle remote_handle = buffer_pool_->ShareToProcess( 552 buffer->id(), client->render_process_handle, &memory_size); 553 client->event_handler->OnBufferCreated( 554 client->controller_id, remote_handle, memory_size, buffer->id()); 555 } 556 557 client->event_handler->OnBufferReady( 558 client->controller_id, buffer->id(), timestamp, frame_format); 559 bool inserted = client->active_buffers.insert(buffer->id()).second; 560 DCHECK(inserted) << "Unexpected duplicate buffer: " << buffer->id(); 561 count++; 562 } 563 } 564 565 buffer_pool_->HoldForConsumers(buffer->id(), count); 566} 567 568void VideoCaptureController::DoErrorOnIOThread() { 569 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); 570 state_ = VIDEO_CAPTURE_STATE_ERROR; 571 572 for (ControllerClients::iterator client_it = controller_clients_.begin(); 573 client_it != controller_clients_.end(); ++client_it) { 574 ControllerClient* client = *client_it; 575 if (client->session_closed) 576 continue; 577 578 client->event_handler->OnError(client->controller_id); 579 } 580} 581 582void VideoCaptureController::DoBufferDestroyedOnIOThread( 583 int buffer_id_to_drop) { 584 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); 585 586 for (ControllerClients::iterator client_it = controller_clients_.begin(); 587 client_it != controller_clients_.end(); ++client_it) { 588 ControllerClient* client = *client_it; 589 if (client->session_closed) 590 continue; 591 592 if (client->known_buffers.erase(buffer_id_to_drop)) { 593 client->event_handler->OnBufferDestroyed(client->controller_id, 594 buffer_id_to_drop); 595 } 596 } 597} 598 599VideoCaptureController::ControllerClient* 600VideoCaptureController::FindClient( 601 const VideoCaptureControllerID& id, 602 VideoCaptureControllerEventHandler* handler, 603 const ControllerClients& clients) { 604 for (ControllerClients::const_iterator client_it = clients.begin(); 605 client_it != clients.end(); ++client_it) { 606 if ((*client_it)->controller_id == id && 607 (*client_it)->event_handler == handler) { 608 return *client_it; 609 } 610 } 611 return NULL; 612} 613 614VideoCaptureController::ControllerClient* 615VideoCaptureController::FindClient( 616 int session_id, 617 const ControllerClients& clients) { 618 for (ControllerClients::const_iterator client_it = clients.begin(); 619 client_it != clients.end(); ++client_it) { 620 if ((*client_it)->session_id == session_id) { 621 return *client_it; 622 } 623 } 624 return NULL; 625} 626 627int VideoCaptureController::GetClientCount() { 628 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); 629 return controller_clients_.size(); 630} 631 632} // namespace content 633