Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright 2013 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 #include <cstring> | 5 #include <cstring> |
| 6 #include <map> | |
| 7 #include <string> | 6 #include <string> |
| 8 #include <utility> | |
| 9 #include <vector> | 7 #include <vector> |
| 10 | 8 |
| 11 #include "base/basictypes.h" | 9 #include "base/basictypes.h" |
| 12 #include "base/compiler_specific.h" | 10 #include "base/compiler_specific.h" |
| 13 #include "build/build_config.h" | 11 #include "build/build_config.h" |
| 14 #include "media/cdm/ppapi/api/content_decryption_module.h" | 12 #include "media/cdm/ppapi/api/content_decryption_module.h" |
| 13 #include "media/cdm/ppapi/cdm_adapter.h" | |
| 14 #include "media/cdm/ppapi/cdm_helpers.h" | |
| 15 #include "media/cdm/ppapi/linked_ptr.h" | 15 #include "media/cdm/ppapi/linked_ptr.h" |
| 16 #include "ppapi/c/pp_completion_callback.h" | 16 #include "ppapi/c/pp_completion_callback.h" |
| 17 #include "ppapi/c/pp_errors.h" | 17 #include "ppapi/c/pp_errors.h" |
| 18 #include "ppapi/c/pp_stdint.h" | 18 #include "ppapi/c/pp_stdint.h" |
| 19 #include "ppapi/c/private/pp_content_decryptor.h" | 19 #include "ppapi/c/private/pp_content_decryptor.h" |
| 20 #include "ppapi/cpp/completion_callback.h" | 20 #include "ppapi/cpp/completion_callback.h" |
| 21 #include "ppapi/cpp/core.h" | 21 #include "ppapi/cpp/core.h" |
| 22 #include "ppapi/cpp/dev/buffer_dev.h" | |
| 23 #include "ppapi/cpp/instance.h" | 22 #include "ppapi/cpp/instance.h" |
| 24 #include "ppapi/cpp/logging.h" | 23 #include "ppapi/cpp/logging.h" |
| 25 #include "ppapi/cpp/module.h" | 24 #include "ppapi/cpp/module.h" |
| 26 #include "ppapi/cpp/pass_ref.h" | 25 #include "ppapi/cpp/pass_ref.h" |
| 27 #include "ppapi/cpp/private/content_decryptor_private.h" | 26 #include "ppapi/cpp/private/content_decryptor_private.h" |
| 28 #include "ppapi/cpp/resource.h" | 27 #include "ppapi/cpp/resource.h" |
| 29 #include "ppapi/cpp/var.h" | 28 #include "ppapi/cpp/var.h" |
| 30 #include "ppapi/cpp/var_array_buffer.h" | 29 #include "ppapi/cpp/var_array_buffer.h" |
| 31 #include "ppapi/utility/completion_callback_factory.h" | 30 #include "ppapi/utility/completion_callback_factory.h" |
| 32 | 31 |
| (...skipping 173 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 206 } | 205 } |
| 207 | 206 |
| 208 PP_NOTREACHED(); | 207 PP_NOTREACHED(); |
| 209 return cdm::kStreamTypeVideo; | 208 return cdm::kStreamTypeVideo; |
| 210 } | 209 } |
| 211 | 210 |
| 212 } // namespace | 211 } // namespace |
| 213 | 212 |
| 214 namespace media { | 213 namespace media { |
| 215 | 214 |
| 216 // cdm::Buffer implementation that provides access to memory owned by a | |
| 217 // pp::Buffer_Dev. | |
| 218 // This class holds a reference to the Buffer_Dev throughout its lifetime. | |
| 219 // TODO(xhwang): Find a better name. It's confusing to have PpbBuffer, | |
| 220 // pp::Buffer_Dev and PPB_Buffer_Dev. | |
| 221 class PpbBuffer : public cdm::Buffer { | |
| 222 public: | |
| 223 static PpbBuffer* Create(const pp::Buffer_Dev& buffer, uint32_t buffer_id) { | |
| 224 PP_DCHECK(buffer.data()); | |
| 225 PP_DCHECK(buffer.size()); | |
| 226 PP_DCHECK(buffer_id); | |
| 227 return new PpbBuffer(buffer, buffer_id); | |
| 228 } | |
| 229 | |
| 230 // cdm::Buffer implementation. | |
| 231 virtual void Destroy() OVERRIDE { delete this; } | |
| 232 | |
| 233 virtual int32_t Capacity() const OVERRIDE { return buffer_.size(); } | |
| 234 | |
| 235 virtual uint8_t* Data() OVERRIDE { | |
| 236 return static_cast<uint8_t*>(buffer_.data()); | |
| 237 } | |
| 238 | |
| 239 virtual void SetSize(int32_t size) OVERRIDE { | |
| 240 PP_DCHECK(size >= 0); | |
| 241 PP_DCHECK(size < Capacity()); | |
| 242 if (size < 0 || size > Capacity()) { | |
| 243 size_ = 0; | |
| 244 return; | |
| 245 } | |
| 246 | |
| 247 size_ = size; | |
| 248 } | |
| 249 | |
| 250 virtual int32_t Size() const OVERRIDE { return size_; } | |
| 251 | |
| 252 pp::Buffer_Dev buffer_dev() const { return buffer_; } | |
| 253 | |
| 254 uint32_t buffer_id() const { return buffer_id_; } | |
| 255 | |
| 256 private: | |
| 257 PpbBuffer(pp::Buffer_Dev buffer, uint32_t buffer_id) | |
| 258 : buffer_(buffer), | |
| 259 buffer_id_(buffer_id), | |
| 260 size_(0) {} | |
| 261 virtual ~PpbBuffer() {} | |
| 262 | |
| 263 pp::Buffer_Dev buffer_; | |
| 264 uint32_t buffer_id_; | |
| 265 int32_t size_; | |
| 266 | |
| 267 DISALLOW_COPY_AND_ASSIGN(PpbBuffer); | |
| 268 }; | |
| 269 | |
| 270 class PpbBufferAllocator { | |
| 271 public: | |
| 272 explicit PpbBufferAllocator(pp::Instance* instance) | |
| 273 : instance_(instance), | |
| 274 next_buffer_id_(1) {} | |
| 275 ~PpbBufferAllocator() {} | |
| 276 | |
| 277 cdm::Buffer* Allocate(int32_t capacity); | |
| 278 | |
| 279 // Releases the buffer with |buffer_id|. A buffer can be recycled after | |
| 280 // it is released. | |
| 281 void Release(uint32_t buffer_id); | |
| 282 | |
| 283 private: | |
| 284 typedef std::map<uint32_t, pp::Buffer_Dev> AllocatedBufferMap; | |
| 285 typedef std::multimap<int, std::pair<uint32_t, pp::Buffer_Dev> > | |
| 286 FreeBufferMap; | |
| 287 | |
| 288 // Always pad new allocated buffer so that we don't need to reallocate | |
| 289 // buffers frequently if requested sizes fluctuate slightly. | |
| 290 static const int kBufferPadding = 512; | |
| 291 | |
| 292 // Maximum number of free buffers we can keep when allocating new buffers. | |
| 293 static const int kFreeLimit = 3; | |
| 294 | |
| 295 pp::Buffer_Dev AllocateNewBuffer(int capacity); | |
| 296 | |
| 297 pp::Instance* const instance_; | |
| 298 uint32_t next_buffer_id_; | |
| 299 AllocatedBufferMap allocated_buffers_; | |
| 300 FreeBufferMap free_buffers_; | |
| 301 | |
| 302 DISALLOW_COPY_AND_ASSIGN(PpbBufferAllocator); | |
| 303 }; | |
| 304 | |
| 305 cdm::Buffer* PpbBufferAllocator::Allocate(int32_t capacity) { | |
| 306 PP_DCHECK(IsMainThread()); | |
| 307 | |
| 308 if (capacity <= 0) | |
| 309 return NULL; | |
| 310 | |
| 311 pp::Buffer_Dev buffer; | |
| 312 uint32_t buffer_id = 0; | |
| 313 | |
| 314 // Reuse a buffer in the free list if there is one that fits |capacity|. | |
| 315 // Otherwise, create a new one. | |
| 316 FreeBufferMap::iterator found = free_buffers_.lower_bound(capacity); | |
| 317 if (found == free_buffers_.end()) { | |
| 318 // TODO(xhwang): Report statistics about how many new buffers are allocated. | |
| 319 buffer = AllocateNewBuffer(capacity); | |
| 320 if (buffer.is_null()) | |
| 321 return NULL; | |
| 322 buffer_id = next_buffer_id_++; | |
| 323 } else { | |
| 324 buffer = found->second.second; | |
| 325 buffer_id = found->second.first; | |
| 326 free_buffers_.erase(found); | |
| 327 } | |
| 328 | |
| 329 allocated_buffers_.insert(std::make_pair(buffer_id, buffer)); | |
| 330 | |
| 331 return PpbBuffer::Create(buffer, buffer_id); | |
| 332 } | |
| 333 | |
| 334 void PpbBufferAllocator::Release(uint32_t buffer_id) { | |
| 335 if (!buffer_id) | |
| 336 return; | |
| 337 | |
| 338 AllocatedBufferMap::iterator found = allocated_buffers_.find(buffer_id); | |
| 339 if (found == allocated_buffers_.end()) | |
| 340 return; | |
| 341 | |
| 342 pp::Buffer_Dev& buffer = found->second; | |
| 343 free_buffers_.insert( | |
| 344 std::make_pair(buffer.size(), std::make_pair(buffer_id, buffer))); | |
| 345 | |
| 346 allocated_buffers_.erase(found); | |
| 347 } | |
| 348 | |
| 349 pp::Buffer_Dev PpbBufferAllocator::AllocateNewBuffer(int32_t capacity) { | |
| 350 // Destroy the smallest buffer before allocating a new bigger buffer if the | |
| 351 // number of free buffers exceeds a limit. This mechanism helps avoid ending | |
| 352 // up with too many small buffers, which could happen if the size to be | |
| 353 // allocated keeps increasing. | |
| 354 if (free_buffers_.size() >= static_cast<uint32_t>(kFreeLimit)) | |
| 355 free_buffers_.erase(free_buffers_.begin()); | |
| 356 | |
| 357 // Creation of pp::Buffer_Dev is expensive! It involves synchronous IPC calls. | |
| 358 // That's why we try to avoid AllocateNewBuffer() as much as we can. | |
| 359 return pp::Buffer_Dev(instance_, capacity + kBufferPadding); | |
| 360 } | |
| 361 | |
| 362 class DecryptedBlockImpl : public cdm::DecryptedBlock { | |
| 363 public: | |
| 364 DecryptedBlockImpl() : buffer_(NULL), timestamp_(0) {} | |
| 365 virtual ~DecryptedBlockImpl() { if (buffer_) buffer_->Destroy(); } | |
| 366 | |
| 367 virtual void SetDecryptedBuffer(cdm::Buffer* buffer) OVERRIDE { | |
| 368 buffer_ = static_cast<PpbBuffer*>(buffer); | |
| 369 } | |
| 370 virtual cdm::Buffer* DecryptedBuffer() OVERRIDE { return buffer_; } | |
| 371 | |
| 372 virtual void SetTimestamp(int64_t timestamp) OVERRIDE { | |
| 373 timestamp_ = timestamp; | |
| 374 } | |
| 375 virtual int64_t Timestamp() const OVERRIDE { return timestamp_; } | |
| 376 | |
| 377 private: | |
| 378 PpbBuffer* buffer_; | |
| 379 int64_t timestamp_; | |
| 380 | |
| 381 DISALLOW_COPY_AND_ASSIGN(DecryptedBlockImpl); | |
| 382 }; | |
| 383 | |
| 384 class VideoFrameImpl : public cdm::VideoFrame { | |
| 385 public: | |
| 386 VideoFrameImpl(); | |
| 387 virtual ~VideoFrameImpl(); | |
| 388 | |
| 389 virtual void SetFormat(cdm::VideoFormat format) OVERRIDE { | |
| 390 format_ = format; | |
| 391 } | |
| 392 virtual cdm::VideoFormat Format() const OVERRIDE { return format_; } | |
| 393 | |
| 394 virtual void SetSize(cdm::Size size) OVERRIDE { size_ = size; } | |
| 395 virtual cdm::Size Size() const OVERRIDE { return size_; } | |
| 396 | |
| 397 virtual void SetFrameBuffer(cdm::Buffer* frame_buffer) OVERRIDE { | |
| 398 frame_buffer_ = static_cast<PpbBuffer*>(frame_buffer); | |
| 399 } | |
| 400 virtual cdm::Buffer* FrameBuffer() OVERRIDE { return frame_buffer_; } | |
| 401 | |
| 402 virtual void SetPlaneOffset(cdm::VideoFrame::VideoPlane plane, | |
| 403 int32_t offset) OVERRIDE { | |
| 404 PP_DCHECK(0 <= plane && plane < kMaxPlanes); | |
| 405 PP_DCHECK(offset >= 0); | |
| 406 plane_offsets_[plane] = offset; | |
| 407 } | |
| 408 virtual int32_t PlaneOffset(VideoPlane plane) OVERRIDE { | |
| 409 PP_DCHECK(0 <= plane && plane < kMaxPlanes); | |
| 410 return plane_offsets_[plane]; | |
| 411 } | |
| 412 | |
| 413 virtual void SetStride(VideoPlane plane, int32_t stride) OVERRIDE { | |
| 414 PP_DCHECK(0 <= plane && plane < kMaxPlanes); | |
| 415 strides_[plane] = stride; | |
| 416 } | |
| 417 virtual int32_t Stride(VideoPlane plane) OVERRIDE { | |
| 418 PP_DCHECK(0 <= plane && plane < kMaxPlanes); | |
| 419 return strides_[plane]; | |
| 420 } | |
| 421 | |
| 422 virtual void SetTimestamp(int64_t timestamp) OVERRIDE { | |
| 423 timestamp_ = timestamp; | |
| 424 } | |
| 425 virtual int64_t Timestamp() const OVERRIDE { return timestamp_; } | |
| 426 | |
| 427 private: | |
| 428 // The video buffer format. | |
| 429 cdm::VideoFormat format_; | |
| 430 | |
| 431 // Width and height of the video frame. | |
| 432 cdm::Size size_; | |
| 433 | |
| 434 // The video frame buffer. | |
| 435 PpbBuffer* frame_buffer_; | |
| 436 | |
| 437 // Array of data pointers to each plane in the video frame buffer. | |
| 438 int32_t plane_offsets_[kMaxPlanes]; | |
| 439 | |
| 440 // Array of strides for each plane, typically greater or equal to the width | |
| 441 // of the surface divided by the horizontal sampling period. Note that | |
| 442 // strides can be negative. | |
| 443 int32_t strides_[kMaxPlanes]; | |
| 444 | |
| 445 // Presentation timestamp in microseconds. | |
| 446 int64_t timestamp_; | |
| 447 | |
| 448 DISALLOW_COPY_AND_ASSIGN(VideoFrameImpl); | |
| 449 }; | |
| 450 | |
| 451 VideoFrameImpl::VideoFrameImpl() | |
| 452 : format_(cdm::kUnknownVideoFormat), | |
| 453 frame_buffer_(NULL), | |
| 454 timestamp_(0) { | |
| 455 for (int32_t i = 0; i < kMaxPlanes; ++i) { | |
| 456 plane_offsets_[i] = 0; | |
| 457 strides_[i] = 0; | |
| 458 } | |
| 459 } | |
| 460 | |
| 461 VideoFrameImpl::~VideoFrameImpl() { | |
| 462 if (frame_buffer_) | |
| 463 frame_buffer_->Destroy(); | |
| 464 } | |
| 465 | |
| 466 class AudioFramesImpl : public cdm::AudioFrames_1, | |
| 467 public cdm::AudioFrames_2 { | |
| 468 public: | |
| 469 AudioFramesImpl() : buffer_(NULL), format_(cdm::kAudioFormatS16) {} | |
| 470 virtual ~AudioFramesImpl() { | |
| 471 if (buffer_) | |
| 472 buffer_->Destroy(); | |
| 473 } | |
| 474 | |
| 475 // AudioFrames implementation. | |
| 476 virtual void SetFrameBuffer(cdm::Buffer* buffer) OVERRIDE { | |
| 477 buffer_ = static_cast<PpbBuffer*>(buffer); | |
| 478 } | |
| 479 virtual cdm::Buffer* FrameBuffer() OVERRIDE { | |
| 480 return buffer_; | |
| 481 } | |
| 482 virtual void SetFormat(cdm::AudioFormat format) OVERRIDE { | |
| 483 format_ = format; | |
| 484 } | |
| 485 virtual cdm::AudioFormat Format() const OVERRIDE { | |
| 486 return format_; | |
| 487 } | |
| 488 | |
| 489 private: | |
| 490 PpbBuffer* buffer_; | |
| 491 cdm::AudioFormat format_; | |
| 492 | |
| 493 DISALLOW_COPY_AND_ASSIGN(AudioFramesImpl); | |
| 494 }; | |
| 495 | |
| 496 // GetCdmHostFunc implementation. | 215 // GetCdmHostFunc implementation. |
| 497 void* GetCdmHost(int host_interface_version, void* user_data); | 216 void* GetCdmHost(int host_interface_version, void* user_data); |
| 498 | 217 |
| 499 // A wrapper class for abstracting away PPAPI interaction and threading for a | 218 // A wrapper class for abstracting away PPAPI interaction and threading for a |
| 500 // Content Decryption Module (CDM). | 219 // Content Decryption Module (CDM). |
| 501 class CdmWrapper : public pp::Instance, | 220 class CdmWrapper : public pp::Instance, |
|
DaleCurtis
2013/10/17 20:31:58
This being the prototype I'm talking about...
| |
| 502 public pp::ContentDecryptor_Private, | 221 public pp::ContentDecryptor_Private, |
| 503 public cdm::Host_1, | 222 public cdm::Host_1, |
| 504 public cdm::Host_2 { | 223 public cdm::Host_2 { |
| 505 public: | 224 public: |
| 506 CdmWrapper(PP_Instance instance, pp::Module* module); | 225 CdmWrapper(PP_Instance instance, pp::Module* module); |
| 507 virtual ~CdmWrapper(); | 226 virtual ~CdmWrapper(); |
| 508 | 227 |
| 509 // pp::Instance implementation. | 228 // pp::Instance implementation. |
| 510 virtual bool Init(uint32_t argc, const char* argn[], const char* argv[]) { | 229 virtual bool Init(uint32_t argc, const char* argn[], const char* argv[]) { |
| 511 return true; | 230 return true; |
| (...skipping 143 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 655 bool challenge_in_progress_; | 374 bool challenge_in_progress_; |
| 656 | 375 |
| 657 // Same as above, these are only read by QueryOutputProtectionStatusDone(). | 376 // Same as above, these are only read by QueryOutputProtectionStatusDone(). |
| 658 uint32_t output_link_mask_; | 377 uint32_t output_link_mask_; |
| 659 uint32_t output_protection_mask_; | 378 uint32_t output_protection_mask_; |
| 660 bool query_output_protection_in_progress_; | 379 bool query_output_protection_in_progress_; |
| 661 #endif | 380 #endif |
| 662 | 381 |
| 663 PpbBufferAllocator allocator_; | 382 PpbBufferAllocator allocator_; |
| 664 pp::CompletionCallbackFactory<CdmWrapper> callback_factory_; | 383 pp::CompletionCallbackFactory<CdmWrapper> callback_factory_; |
| 665 cdm::ContentDecryptionModule* cdm_; | 384 linked_ptr<CdmAdapter> cdm_; |
| 666 std::string key_system_; | 385 std::string key_system_; |
| 667 | 386 |
| 668 DISALLOW_COPY_AND_ASSIGN(CdmWrapper); | 387 DISALLOW_COPY_AND_ASSIGN(CdmWrapper); |
| 669 }; | 388 }; |
| 670 | 389 |
| 671 CdmWrapper::CdmWrapper(PP_Instance instance, pp::Module* module) | 390 CdmWrapper::CdmWrapper(PP_Instance instance, pp::Module* module) |
| 672 : pp::Instance(instance), | 391 : pp::Instance(instance), |
| 673 pp::ContentDecryptor_Private(this), | 392 pp::ContentDecryptor_Private(this), |
| 674 #if defined(OS_CHROMEOS) | 393 #if defined(OS_CHROMEOS) |
| 675 output_protection_(this), | 394 output_protection_(this), |
| 676 platform_verification_(this), | 395 platform_verification_(this), |
| 677 // Err on the side of the most common case... | 396 // Err on the side of the most common case... |
| 678 can_challenge_platform_(true), | 397 can_challenge_platform_(true), |
| 679 challenge_in_progress_(false), | 398 challenge_in_progress_(false), |
| 680 output_link_mask_(0), | 399 output_link_mask_(0), |
| 681 output_protection_mask_(0), | 400 output_protection_mask_(0), |
| 682 query_output_protection_in_progress_(false), | 401 query_output_protection_in_progress_(false), |
| 683 #endif | 402 #endif |
| 684 allocator_(this), | 403 allocator_(this), |
| 685 cdm_(NULL) { | 404 cdm_(NULL) { |
| 686 callback_factory_.Initialize(this); | 405 callback_factory_.Initialize(this); |
| 687 #if defined(OS_CHROMEOS) | 406 #if defined(OS_CHROMEOS) |
| 688 // Preemptively retrieve the platform challenge status. It will not change. | 407 // Preemptively retrieve the platform challenge status. It will not change. |
| 689 platform_verification_.CanChallengePlatform( | 408 platform_verification_.CanChallengePlatform( |
| 690 callback_factory_.NewCallbackWithOutput( | 409 callback_factory_.NewCallbackWithOutput( |
| 691 &CdmWrapper::CanChallengePlatformDone)); | 410 &CdmWrapper::CanChallengePlatformDone)); |
| 692 #endif | 411 #endif |
| 693 } | 412 } |
| 694 | 413 |
| 695 CdmWrapper::~CdmWrapper() { | 414 CdmWrapper::~CdmWrapper() {} |
| 696 if (cdm_) | |
| 697 cdm_->Destroy(); | |
| 698 } | |
| 699 | 415 |
| 700 bool CdmWrapper::CreateCdmInstance(const std::string& key_system) { | 416 bool CdmWrapper::CreateCdmInstance(const std::string& key_system) { |
| 701 PP_DCHECK(!cdm_); | 417 PP_DCHECK(!cdm_); |
| 702 cdm_ = static_cast<cdm::ContentDecryptionModule*>( | 418 cdm_ = make_linked_ptr(CdmAdapter::Create( |
| 703 ::CreateCdmInstance(cdm::kCdmInterfaceVersion, | 419 key_system.data(), key_system.size(), GetCdmHost, this)); |
| 704 key_system.data(), key_system.size(), | |
| 705 GetCdmHost, this)); | |
| 706 | |
| 707 return (cdm_ != NULL); | 420 return (cdm_ != NULL); |
| 708 } | 421 } |
| 709 | 422 |
| 710 void CdmWrapper::Initialize(const std::string& key_system, | 423 void CdmWrapper::Initialize(const std::string& key_system, |
| 711 bool can_challenge_platform) { | 424 bool can_challenge_platform) { |
| 712 PP_DCHECK(!key_system.empty()); | 425 PP_DCHECK(!key_system.empty()); |
| 713 PP_DCHECK(key_system_.empty() || (key_system_ == key_system && cdm_)); | 426 PP_DCHECK(key_system_.empty() || (key_system_ == key_system && cdm_)); |
| 714 | 427 |
| 715 if (!cdm_) { | 428 if (!cdm_) { |
| 716 if (!CreateCdmInstance(key_system)) { | 429 if (!CreateCdmInstance(key_system)) { |
| (...skipping 676 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 1393 } // namespace media | 1106 } // namespace media |
| 1394 | 1107 |
| 1395 namespace pp { | 1108 namespace pp { |
| 1396 | 1109 |
| 1397 // Factory function for your specialization of the Module object. | 1110 // Factory function for your specialization of the Module object. |
| 1398 Module* CreateModule() { | 1111 Module* CreateModule() { |
| 1399 return new media::CdmWrapperModule(); | 1112 return new media::CdmWrapperModule(); |
| 1400 } | 1113 } |
| 1401 | 1114 |
| 1402 } // namespace pp | 1115 } // namespace pp |
| OLD | NEW |