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. 4 5 #ifndef MEDIA_CDM_PPAPI_CDM_WRAPPER_H_ 6 #define MEDIA_CDM_PPAPI_CDM_WRAPPER_H_ 7 8 #include <map> 9 #include <queue> 10 #include <string> 11 12 #include "base/basictypes.h" 13 #include "media/cdm/ppapi/api/content_decryption_module.h" 14 #include "media/cdm/ppapi/cdm_helpers.h" 15 #include "media/cdm/ppapi/supported_cdm_versions.h" 16 #include "ppapi/cpp/logging.h" 17 18 namespace media { 19 20 // CdmWrapper wraps different versions of ContentDecryptionModule interfaces and 21 // exposes a common interface to the caller. 22 // 23 // The caller should call CdmWrapper::Create() to create a CDM instance. 24 // CdmWrapper will first try to create a CDM instance that supports the latest 25 // CDM interface (ContentDecryptionModule). If such an instance cannot be 26 // created (e.g. an older CDM was loaded), CdmWrapper will try to create a CDM 27 // that supports an older version of CDM interface (e.g. 28 // ContentDecryptionModule_*). Internally CdmWrapper converts the CdmWrapper 29 // calls to corresponding ContentDecryptionModule calls. 30 // 31 // Note that CdmWrapper interface always reflects the latest state of content 32 // decryption related PPAPI APIs (e.g. pp::ContentDecryptor_Private). 33 // 34 // Since this file is highly templated and default implementations are short 35 // (just a shim layer in most cases), everything is done in this header file. 36 class CdmWrapper { 37 public: 38 static CdmWrapper* Create(const char* key_system, 39 uint32_t key_system_size, 40 GetCdmHostFunc get_cdm_host_func, 41 void* user_data); 42 43 virtual ~CdmWrapper() {}; 44 45 // TODO(jrummell): Remove return value when CDM4/5 are removed. 46 virtual bool SetServerCertificate(uint32_t promise_id, 47 const uint8_t* server_certificate_data, 48 uint32_t server_certificate_data_size) = 0; 49 virtual void CreateSession(uint32_t promise_id, 50 const char* init_data_type, 51 uint32_t init_data_type_size, 52 const uint8_t* init_data, 53 uint32_t init_data_size, 54 cdm::SessionType session_type) = 0; 55 virtual void LoadSession(uint32_t promise_id, 56 const char* web_session_id, 57 uint32_t web_session_id_size) = 0; 58 virtual void UpdateSession(uint32_t promise_id, 59 const char* web_session_id, 60 uint32_t web_session_id_size, 61 const uint8_t* response, 62 uint32_t response_size) = 0; 63 // TODO(jrummell): Remove return value when CDM4/5 are removed. 64 virtual bool CloseSession(uint32_t promise_id, 65 const char* web_session_id, 66 uint32_t web_session_id_size) = 0; 67 virtual void RemoveSession(uint32_t promise_id, 68 const char* web_session_id, 69 uint32_t web_session_id_size) = 0; 70 // TODO(jrummell): Remove return value when CDM4/5 are removed. 71 virtual bool GetUsableKeyIds(uint32_t promise_id, 72 const char* web_session_id, 73 uint32_t web_session_id_size) = 0; 74 virtual void TimerExpired(void* context) = 0; 75 virtual cdm::Status Decrypt(const cdm::InputBuffer& encrypted_buffer, 76 cdm::DecryptedBlock* decrypted_buffer) = 0; 77 virtual cdm::Status InitializeAudioDecoder( 78 const cdm::AudioDecoderConfig& audio_decoder_config) = 0; 79 virtual cdm::Status InitializeVideoDecoder( 80 const cdm::VideoDecoderConfig& video_decoder_config) = 0; 81 virtual void DeinitializeDecoder(cdm::StreamType decoder_type) = 0; 82 virtual void ResetDecoder(cdm::StreamType decoder_type) = 0; 83 virtual cdm::Status DecryptAndDecodeFrame( 84 const cdm::InputBuffer& encrypted_buffer, 85 cdm::VideoFrame* video_frame) = 0; 86 virtual cdm::Status DecryptAndDecodeSamples( 87 const cdm::InputBuffer& encrypted_buffer, 88 cdm::AudioFrames* audio_frames) = 0; 89 virtual void OnPlatformChallengeResponse( 90 const cdm::PlatformChallengeResponse& response) = 0; 91 virtual void OnQueryOutputProtectionStatus( 92 uint32_t link_mask, 93 uint32_t output_protection_mask) = 0; 94 95 // Helper function for the cdm::Host_4 methods. Calls to CreateSession(), 96 // LoadSession(), UpdateSession(), and ReleaseSession() pass in promise ids, 97 // but the CDM interface needs session ids. For create and load, we need to 98 // create a new session_id to pass to the CDM. For update and release, we need 99 // to look up |web_session_id| and convert it into the existing |session_id|. 100 // Since the callbacks don't come through this interface, cdm_adapter needs to 101 // create the mapping (and delete it on release). 102 // TODO(jrummell): Remove these once Host_4 interface is removed. 103 virtual uint32_t LookupPromiseId(uint32_t session_id) = 0; 104 virtual void AssignWebSessionId(uint32_t session_id, 105 const char* web_session_id, 106 uint32_t web_session_id_size) = 0; 107 virtual std::string LookupWebSessionId(uint32_t session_id) = 0; 108 virtual void DropWebSessionId(std::string web_session_id) = 0; 109 110 // Helper functions for the cdm::Host_4 methods. 111 // CDMs using cdm::Host_6 will call OnSessionUsableKeys() as necessary when 112 // resolving LoadSession() and UpdateSession(). This needs to be simulated 113 // for the older CDMs. These must not be called for cdm::Host_6 and later. 114 // TODO(jrummell): Remove these once Host_4 interface is removed. 115 116 // Query whether a SessionUsableKeys event is necessary for the specified 117 // |promise_id|. Returns true if needed and |web_session_id| is updated, 118 // otherwise returns false. 119 virtual bool SessionUsableKeysEventNeeded(uint32_t promise_id, 120 std::string* web_session_id) = 0; 121 122 // Used to indicate that a SessionUsableKeys event is required for the 123 // specified |promise_id| and associated |web_session_id|. 124 virtual void SetSessionUsableKeysEventNeeded( 125 uint32_t promise_id, 126 const char* web_session_id, 127 uint32_t web_session_id_size) = 0; 128 129 // cdm::Host_6 introduces InputBuffer_2 (aka InputBuffer). cdm::Host_4 130 // methods still use InputBuffer_1, so this helper function 131 // converts InputBuffer_2 to InputBuffer_1. 132 // TODO(jrummell): Remove these once Host_4 interfaces is removed. 133 virtual void ConvertInputBuffer(const cdm::InputBuffer& v2, 134 cdm::InputBuffer_1* v1) = 0; 135 136 // Prior to CDM_6, |init_data_type| was a content type. This helper convererts 137 // an |init_data_type| to a content type. 138 // TODO(sandersd): Remove once Host_4 interface is removed. 139 virtual std::string ConvertInitDataTypeToContentType( 140 const std::string& init_data_type) const = 0; 141 142 protected: 143 CdmWrapper() {} 144 145 private: 146 DISALLOW_COPY_AND_ASSIGN(CdmWrapper); 147 }; 148 149 // Template class that does the CdmWrapper -> CdmInterface conversion. Default 150 // implementations are provided. Any methods that need special treatment should 151 // be specialized. 152 template <class CdmInterface> 153 class CdmWrapperImpl : public CdmWrapper { 154 public: 155 static CdmWrapper* Create(const char* key_system, 156 uint32_t key_system_size, 157 GetCdmHostFunc get_cdm_host_func, 158 void* user_data) { 159 void* cdm_instance = ::CreateCdmInstance( 160 CdmInterface::kVersion, key_system, key_system_size, get_cdm_host_func, 161 user_data); 162 if (!cdm_instance) 163 return NULL; 164 165 return new CdmWrapperImpl<CdmInterface>( 166 static_cast<CdmInterface*>(cdm_instance)); 167 } 168 169 virtual ~CdmWrapperImpl() { 170 cdm_->Destroy(); 171 } 172 173 // Returns true if |data| is prefixed with |header| and has data after the 174 // |header|. 175 bool HasHeader(const uint8* data, 176 int data_length, 177 const std::string& header) { 178 return static_cast<size_t>(data_length) > header.length() && 179 std::equal(data, data + header.length(), header.begin()); 180 } 181 182 virtual bool SetServerCertificate( 183 uint32_t promise_id, 184 const uint8_t* server_certificate_data, 185 uint32_t server_certificate_data_size) OVERRIDE { 186 cdm_->SetServerCertificate( 187 promise_id, server_certificate_data, server_certificate_data_size); 188 return true; 189 } 190 191 virtual void CreateSession(uint32_t promise_id, 192 const char* init_data_type, 193 uint32_t init_data_type_size, 194 const uint8_t* init_data, 195 uint32_t init_data_size, 196 cdm::SessionType session_type) OVERRIDE { 197 // TODO(jrummell): Remove this code once |session_type| is passed through 198 // Pepper. When removing, add the header back in for CDM4. 199 PP_DCHECK(session_type == cdm::kTemporary); 200 const char kPersistentSessionHeader[] = "PERSISTENT|"; 201 if (HasHeader(init_data, init_data_size, kPersistentSessionHeader)) { 202 cdm_->CreateSession(promise_id, 203 init_data_type, 204 init_data_type_size, 205 init_data + strlen(kPersistentSessionHeader), 206 init_data_size - strlen(kPersistentSessionHeader), 207 cdm::kPersistent); 208 return; 209 } 210 211 cdm_->CreateSession(promise_id, 212 init_data_type, 213 init_data_type_size, 214 init_data, 215 init_data_size, 216 session_type); 217 } 218 219 virtual void LoadSession(uint32_t promise_id, 220 const char* web_session_id, 221 uint32_t web_session_id_size) OVERRIDE { 222 cdm_->LoadSession(promise_id, web_session_id, web_session_id_size); 223 } 224 225 virtual void UpdateSession(uint32_t promise_id, 226 const char* web_session_id, 227 uint32_t web_session_id_size, 228 const uint8_t* response, 229 uint32_t response_size) OVERRIDE { 230 cdm_->UpdateSession(promise_id, 231 web_session_id, 232 web_session_id_size, 233 response, 234 response_size); 235 } 236 237 virtual bool CloseSession(uint32_t promise_id, 238 const char* web_session_id, 239 uint32_t web_session_id_size) OVERRIDE { 240 cdm_->CloseSession(promise_id, web_session_id, web_session_id_size); 241 return true; 242 } 243 244 virtual void RemoveSession(uint32_t promise_id, 245 const char* web_session_id, 246 uint32_t web_session_id_size) OVERRIDE { 247 cdm_->RemoveSession(promise_id, web_session_id, web_session_id_size); 248 } 249 250 virtual bool GetUsableKeyIds(uint32_t promise_id, 251 const char* web_session_id, 252 uint32_t web_session_id_size) OVERRIDE { 253 cdm_->GetUsableKeyIds(promise_id, web_session_id, web_session_id_size); 254 return true; 255 } 256 257 virtual void TimerExpired(void* context) OVERRIDE { 258 cdm_->TimerExpired(context); 259 } 260 261 virtual cdm::Status Decrypt(const cdm::InputBuffer& encrypted_buffer, 262 cdm::DecryptedBlock* decrypted_buffer) OVERRIDE { 263 return cdm_->Decrypt(encrypted_buffer, decrypted_buffer); 264 } 265 266 virtual cdm::Status InitializeAudioDecoder( 267 const cdm::AudioDecoderConfig& audio_decoder_config) OVERRIDE { 268 return cdm_->InitializeAudioDecoder(audio_decoder_config); 269 } 270 271 virtual cdm::Status InitializeVideoDecoder( 272 const cdm::VideoDecoderConfig& video_decoder_config) OVERRIDE { 273 return cdm_->InitializeVideoDecoder(video_decoder_config); 274 } 275 276 virtual void DeinitializeDecoder(cdm::StreamType decoder_type) OVERRIDE { 277 cdm_->DeinitializeDecoder(decoder_type); 278 } 279 280 virtual void ResetDecoder(cdm::StreamType decoder_type) OVERRIDE { 281 cdm_->ResetDecoder(decoder_type); 282 } 283 284 virtual cdm::Status DecryptAndDecodeFrame( 285 const cdm::InputBuffer& encrypted_buffer, 286 cdm::VideoFrame* video_frame) OVERRIDE { 287 return cdm_->DecryptAndDecodeFrame(encrypted_buffer, video_frame); 288 } 289 290 virtual cdm::Status DecryptAndDecodeSamples( 291 const cdm::InputBuffer& encrypted_buffer, 292 cdm::AudioFrames* audio_frames) OVERRIDE { 293 return cdm_->DecryptAndDecodeSamples(encrypted_buffer, audio_frames); 294 } 295 296 virtual void OnPlatformChallengeResponse( 297 const cdm::PlatformChallengeResponse& response) OVERRIDE { 298 cdm_->OnPlatformChallengeResponse(response); 299 } 300 301 virtual void OnQueryOutputProtectionStatus( 302 uint32_t link_mask, 303 uint32_t output_protection_mask) OVERRIDE { 304 cdm_->OnQueryOutputProtectionStatus(link_mask, output_protection_mask); 305 } 306 307 uint32_t CreateSessionId() { 308 return next_session_id_++; 309 } 310 311 void RegisterPromise(uint32_t session_id, uint32_t promise_id) { 312 PP_DCHECK(promise_to_session_id_map_.find(session_id) == 313 promise_to_session_id_map_.end()); 314 promise_to_session_id_map_.insert(std::make_pair(session_id, promise_id)); 315 } 316 317 virtual uint32_t LookupPromiseId(uint32_t session_id) { 318 std::map<uint32_t, uint32_t>::iterator it = 319 promise_to_session_id_map_.find(session_id); 320 if (it == promise_to_session_id_map_.end()) 321 return 0; 322 uint32_t promise_id = it->second; 323 promise_to_session_id_map_.erase(it); 324 return promise_id; 325 } 326 327 virtual void AssignWebSessionId(uint32_t session_id, 328 const char* web_session_id, 329 uint32_t web_session_id_size) { 330 web_session_to_session_id_map_.insert(std::make_pair( 331 std::string(web_session_id, web_session_id_size), session_id)); 332 } 333 334 uint32_t LookupSessionId(std::string web_session_id) { 335 return web_session_to_session_id_map_.find(web_session_id)->second; 336 } 337 338 virtual std::string LookupWebSessionId(uint32_t session_id) { 339 std::map<std::string, uint32_t>::iterator it; 340 for (it = web_session_to_session_id_map_.begin(); 341 it != web_session_to_session_id_map_.end(); 342 ++it) { 343 if (it->second == session_id) 344 return it->first; 345 } 346 PP_NOTREACHED(); 347 return std::string(); 348 } 349 350 virtual void DropWebSessionId(std::string web_session_id) { 351 web_session_to_session_id_map_.erase(web_session_id); 352 } 353 354 virtual bool SessionUsableKeysEventNeeded(uint32_t promise_id, 355 std::string* web_session_id) { 356 std::map<uint32_t, std::string>::iterator it = 357 promises_needing_usable_keys_event_.find(promise_id); 358 if (it == promises_needing_usable_keys_event_.end()) 359 return false; 360 web_session_id->swap(it->second); 361 promises_needing_usable_keys_event_.erase(it); 362 return true; 363 } 364 365 virtual void SetSessionUsableKeysEventNeeded(uint32_t promise_id, 366 const char* web_session_id, 367 uint32_t web_session_id_size) { 368 promises_needing_usable_keys_event_.insert(std::make_pair( 369 promise_id, std::string(web_session_id, web_session_id_size))); 370 } 371 372 virtual void ConvertInputBuffer(const cdm::InputBuffer& v2, 373 cdm::InputBuffer_1* v1) { 374 v1->data = v2.data; 375 v1->data_size = v2.data_size; 376 v1->data_offset = 0; 377 v1->key_id = v2.key_id; 378 v1->key_id_size = v2.key_id_size; 379 v1->iv = v2.iv; 380 v1->iv_size = v2.iv_size; 381 v1->subsamples = v2.subsamples; 382 v1->num_subsamples = v2.num_subsamples; 383 v1->timestamp = v2.timestamp; 384 } 385 386 virtual std::string ConvertInitDataTypeToContentType( 387 const std::string& init_data_type) const { 388 if (init_data_type == "cenc") 389 return "video/mp4"; 390 if (init_data_type == "webm") 391 return "video/webm"; 392 return init_data_type; 393 } 394 395 private: 396 CdmWrapperImpl(CdmInterface* cdm) : cdm_(cdm), next_session_id_(100) { 397 PP_DCHECK(cdm_); 398 } 399 400 CdmInterface* cdm_; 401 402 std::map<uint32_t, uint32_t> promise_to_session_id_map_; 403 uint32_t next_session_id_; 404 std::map<std::string, uint32_t> web_session_to_session_id_map_; 405 406 std::map<uint32_t, std::string> promises_needing_usable_keys_event_; 407 408 DISALLOW_COPY_AND_ASSIGN(CdmWrapperImpl); 409 }; 410 411 // Overrides for the cdm::Host_4 methods. Calls to CreateSession(), 412 // LoadSession(), UpdateSession(), and ReleaseSession() pass in promise ids, 413 // but the CDM interface needs session ids. For create and load, we need to 414 // create a new session_id to pass to the CDM. For update and release, we need 415 // to look up |web_session_id| and convert it into the existing |session_id|. 416 // Since the callbacks don't come through this interface, cdm_adapter needs to 417 // create the mapping (and delete it on release). Finally, for create, we need 418 // to translate |init_data_type| to a MIME type. 419 // TODO(jrummell): Remove these once Host_4 interface is removed. 420 421 template <> 422 bool CdmWrapperImpl<cdm::ContentDecryptionModule_4>::SetServerCertificate( 423 uint32_t promise_id, 424 const uint8_t* server_certificate_data, 425 uint32_t server_certificate_data_size) { 426 return false; 427 } 428 429 template <> 430 void CdmWrapperImpl<cdm::ContentDecryptionModule_4>::CreateSession( 431 uint32_t promise_id, 432 const char* init_data_type, 433 uint32_t init_data_type_size, 434 const uint8_t* init_data, 435 uint32_t init_data_size, 436 cdm::SessionType session_type) { 437 uint32_t session_id = CreateSessionId(); 438 RegisterPromise(session_id, promise_id); 439 std::string converted_init_data_type = ConvertInitDataTypeToContentType( 440 std::string(init_data_type, init_data_type_size)); 441 cdm_->CreateSession(session_id, 442 converted_init_data_type.data(), 443 converted_init_data_type.length(), 444 init_data, 445 init_data_size); 446 } 447 448 template <> 449 void CdmWrapperImpl<cdm::ContentDecryptionModule_4>::LoadSession( 450 uint32_t promise_id, 451 const char* web_session_id, 452 uint32_t web_session_id_size) { 453 uint32_t session_id = CreateSessionId(); 454 RegisterPromise(session_id, promise_id); 455 // As CDM_4 doesn't support OnSessionUsableKeysChange(), make sure to generate 456 // one when the promise is resolved. This may be overly aggressive. 457 SetSessionUsableKeysEventNeeded( 458 promise_id, web_session_id, web_session_id_size); 459 cdm_->LoadSession(session_id, web_session_id, web_session_id_size); 460 } 461 462 template <> 463 void CdmWrapperImpl<cdm::ContentDecryptionModule_4>::UpdateSession( 464 uint32_t promise_id, 465 const char* web_session_id, 466 uint32_t web_session_id_size, 467 const uint8_t* response, 468 uint32_t response_size) { 469 std::string web_session_str(web_session_id, web_session_id_size); 470 uint32_t session_id = LookupSessionId(web_session_str); 471 RegisterPromise(session_id, promise_id); 472 // As CDM_4 doesn't support OnSessionUsableKeysChange(), make sure to generate 473 // one when the promise is resolved. This may be overly aggressive. 474 SetSessionUsableKeysEventNeeded( 475 promise_id, web_session_id, web_session_id_size); 476 cdm_->UpdateSession(session_id, response, response_size); 477 } 478 479 template <> 480 bool CdmWrapperImpl<cdm::ContentDecryptionModule_4>::CloseSession( 481 uint32_t promise_id, 482 const char* web_session_id, 483 uint32_t web_session_id_size) { 484 return false; 485 } 486 487 template <> 488 void CdmWrapperImpl<cdm::ContentDecryptionModule_4>::RemoveSession( 489 uint32_t promise_id, 490 const char* web_session_id, 491 uint32_t web_session_id_size) { 492 std::string web_session_str(web_session_id, web_session_id_size); 493 uint32_t session_id = LookupSessionId(web_session_str); 494 RegisterPromise(session_id, promise_id); 495 cdm_->ReleaseSession(session_id); 496 } 497 498 template <> 499 bool CdmWrapperImpl<cdm::ContentDecryptionModule_4>::GetUsableKeyIds( 500 uint32_t promise_id, 501 const char* web_session_id, 502 uint32_t web_session_id_size) { 503 return false; 504 } 505 506 template <> 507 cdm::Status CdmWrapperImpl<cdm::ContentDecryptionModule_4>::Decrypt( 508 const cdm::InputBuffer& encrypted_buffer, 509 cdm::DecryptedBlock* decrypted_buffer) { 510 cdm::InputBuffer_1 buffer; 511 ConvertInputBuffer(encrypted_buffer, &buffer); 512 return cdm_->Decrypt(buffer, decrypted_buffer); 513 } 514 515 template <> 516 cdm::Status 517 CdmWrapperImpl<cdm::ContentDecryptionModule_4>::DecryptAndDecodeFrame( 518 const cdm::InputBuffer& encrypted_buffer, 519 cdm::VideoFrame* video_frame) { 520 cdm::InputBuffer_1 buffer; 521 ConvertInputBuffer(encrypted_buffer, &buffer); 522 return cdm_->DecryptAndDecodeFrame(buffer, video_frame); 523 } 524 525 template <> 526 cdm::Status 527 CdmWrapperImpl<cdm::ContentDecryptionModule_4>::DecryptAndDecodeSamples( 528 const cdm::InputBuffer& encrypted_buffer, 529 cdm::AudioFrames* audio_frames) { 530 cdm::InputBuffer_1 buffer; 531 ConvertInputBuffer(encrypted_buffer, &buffer); 532 return cdm_->DecryptAndDecodeSamples(buffer, audio_frames); 533 } 534 535 CdmWrapper* CdmWrapper::Create(const char* key_system, 536 uint32_t key_system_size, 537 GetCdmHostFunc get_cdm_host_func, 538 void* user_data) { 539 COMPILE_ASSERT(cdm::ContentDecryptionModule::kVersion == 540 cdm::ContentDecryptionModule_6::kVersion, 541 update_code_below); 542 543 // Ensure IsSupportedCdmInterfaceVersion() matches this implementation. 544 // Always update this DCHECK when updating this function. 545 // If this check fails, update this function and DCHECK or update 546 // IsSupportedCdmInterfaceVersion(). 547 PP_DCHECK( 548 !IsSupportedCdmInterfaceVersion(cdm::ContentDecryptionModule::kVersion + 549 1) && 550 IsSupportedCdmInterfaceVersion(cdm::ContentDecryptionModule::kVersion) && 551 IsSupportedCdmInterfaceVersion( 552 cdm::ContentDecryptionModule_4::kVersion) && 553 !IsSupportedCdmInterfaceVersion(cdm::ContentDecryptionModule_4::kVersion - 554 1)); 555 556 // Try to create the CDM using the latest CDM interface version. 557 CdmWrapper* cdm_wrapper = 558 CdmWrapperImpl<cdm::ContentDecryptionModule>::Create( 559 key_system, key_system_size, get_cdm_host_func, user_data); 560 if (cdm_wrapper) 561 return cdm_wrapper; 562 563 // If |cdm_wrapper| is NULL, try to create the CDM using older supported 564 // versions of the CDM interface. 565 cdm_wrapper = CdmWrapperImpl<cdm::ContentDecryptionModule_4>::Create( 566 key_system, key_system_size, get_cdm_host_func, user_data); 567 return cdm_wrapper; 568 } 569 570 // When updating the CdmAdapter, ensure you've updated the CdmWrapper to contain 571 // stub implementations for new or modified methods that the older CDM interface 572 // does not have. 573 // Also update supported_cdm_versions.h. 574 COMPILE_ASSERT(cdm::ContentDecryptionModule::kVersion == 575 cdm::ContentDecryptionModule_6::kVersion, 576 ensure_cdm_wrapper_templates_have_old_version_support); 577 578 } // namespace media 579 580 #endif // MEDIA_CDM_PPAPI_CDM_WRAPPER_H_ 581